Compare commits

..

17 Commits

Author SHA1 Message Date
Franck Nijhof 00627b82e0 2025.5.1 (#144564) 2025-05-09 17:03:40 +02:00
Franck Nijhof 13aba6201e Bump version to 2025.5.1 2025-05-09 13:29:29 +00:00
starkillerOG f392e0c1c7 Prevent errors during cleaning of connections/identifiers in device registry (#144558) 2025-05-09 13:28:33 +00:00
starkillerOG 181eca6c82 Reolink clean device registry mac (#144554) 2025-05-09 13:28:32 +00:00
Bram Kragten 196d923ac6 Update frontend to 20250509.0 (#144549) 2025-05-09 13:28:30 +00:00
Josef Zweck 4ad387c967 Fix statistics coordinator subscription for lamarzocco (#144541) 2025-05-09 13:28:29 +00:00
J. Nick Koston cb475bf153 Bump aiodns to 3.4.0 (#144511) 2025-05-09 13:28:28 +00:00
Michael 47acceea08 Fix removing of smarthome templates on startup of AVM Fritz!SmartHome integration (#144506) 2025-05-09 13:28:26 +00:00
J. Nick Koston fd6fb7e3bc Bump forecast-solar to 4.2.0 (#144502) 2025-05-09 13:28:25 +00:00
Erik Montnemery 30f7e9b441 Don't encrypt or decrypt unknown files in backup archives (#144495) 2025-05-09 13:28:24 +00:00
Matthias Alphart a8beec2691 Ignore Fronius Gen24 firmware 1.35.4-1 SSL verification issue (#144463) 2025-05-09 13:28:23 +00:00
Fredrik Erlandsson 23244fb79f Fix point import error (#144462)
* fix import error

* fix failing tests

* Apply suggestions from code review

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2025-05-09 13:28:22 +00:00
Martin Hjelmare e5c56629e2 Fix Z-Wave reset accumulated values button entity category (#144459) 2025-05-09 13:28:20 +00:00
Josef Zweck a793503c8a Bump pylamarzocco to 2.0.1 (#144454) 2025-05-09 13:28:19 +00:00
DukeChocula 054c7a0adc Add LAP-V102S-AUSR to VeSync (#144437)
Update const.py

Added LAP-V102S-AUSR to Vital 100S
2025-05-09 13:28:18 +00:00
Tamer Wahba 6eb2d1aa7c fix homekit air purifier temperature sensor to convert unit (#144435) 2025-05-09 13:28:16 +00:00
Martin Hjelmare 619fdea5df Fix Z-Wave restore nvm command to wait for driver ready (#144413) 2025-05-09 13:28:15 +00:00
1272 changed files with 8444 additions and 39353 deletions
+28 -32
View File
@@ -37,10 +37,10 @@ on:
type: boolean
env:
CACHE_VERSION: 2
CACHE_VERSION: 12
UV_CACHE_VERSION: 1
MYPY_CACHE_VERSION: 1
HA_SHORT_VERSION: "2025.6"
MYPY_CACHE_VERSION: 9
HA_SHORT_VERSION: "2025.5"
DEFAULT_PYTHON: "3.13"
ALL_PYTHON_VERSIONS: "['3.13']"
# 10.3 is the oldest supported version
@@ -259,7 +259,7 @@ jobs:
with:
path: venv
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{
needs.info.outputs.pre-commit_cache_key }}
- name: Create Python virtual environment
if: steps.cache-venv.outputs.cache-hit != 'true'
@@ -276,7 +276,7 @@ jobs:
path: ${{ env.PRE_COMMIT_CACHE }}
lookup-only: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.pre-commit_cache_key }}
- name: Install pre-commit dependencies
if: steps.cache-precommit.outputs.cache-hit != 'true'
@@ -306,7 +306,7 @@ jobs:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
@@ -315,7 +315,7 @@ jobs:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.pre-commit_cache_key }}
- name: Run ruff-format
run: |
@@ -346,7 +346,7 @@ jobs:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
@@ -355,7 +355,7 @@ jobs:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.pre-commit_cache_key }}
- name: Run ruff
run: |
@@ -386,7 +386,7 @@ jobs:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-venv-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{
needs.info.outputs.pre-commit_cache_key }}
- name: Restore pre-commit environment from cache
id: cache-precommit
@@ -395,7 +395,7 @@ jobs:
path: ${{ env.PRE_COMMIT_CACHE }}
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.pre-commit_cache_key }}
- name: Register yamllint problem matcher
@@ -501,7 +501,7 @@ jobs:
with:
path: venv
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Restore uv wheel cache
if: steps.cache-venv.outputs.cache-hit != 'true'
@@ -509,10 +509,10 @@ jobs:
with:
path: ${{ env.UV_CACHE_DIR }}
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
steps.generate-uv-key.outputs.key }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-uv-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-uv-${{
env.UV_CACHE_VERSION }}-${{ steps.generate-uv-key.outputs.version }}-${{
env.HA_SHORT_VERSION }}-
- name: Install additional OS dependencies
@@ -598,7 +598,7 @@ jobs:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Run hassfest
run: |
@@ -631,7 +631,7 @@ jobs:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Run gen_requirements_all.py
run: |
@@ -653,7 +653,7 @@ jobs:
- name: Check out code from GitHub
uses: actions/checkout@v4.2.2
- name: Dependency review
uses: actions/dependency-review-action@v4.7.0
uses: actions/dependency-review-action@v4.6.0
with:
license-check: false # We use our own license audit checks
@@ -688,7 +688,7 @@ jobs:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Extract license data
run: |
@@ -731,7 +731,7 @@ jobs:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Register pylint problem matcher
run: |
@@ -778,7 +778,7 @@ jobs:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Register pylint problem matcher
run: |
@@ -830,17 +830,17 @@ jobs:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Restore mypy cache
uses: actions/cache@v4.2.3
with:
path: .mypy_cache
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
steps.generate-mypy-key.outputs.key }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-mypy-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-mypy-${{
env.MYPY_CACHE_VERSION }}-${{ steps.generate-mypy-key.outputs.version }}-${{
env.HA_SHORT_VERSION }}-
- name: Register mypy problem matcher
@@ -900,7 +900,7 @@ jobs:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Run split_tests.py
run: |
@@ -959,8 +959,7 @@ jobs:
with:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Register Python problem matcher
run: |
@@ -1085,8 +1084,7 @@ jobs:
with:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Register Python problem matcher
run: |
@@ -1220,8 +1218,7 @@ jobs:
with:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Register Python problem matcher
run: |
@@ -1372,8 +1369,7 @@ jobs:
with:
path: venv
fail-on-cache-miss: true
key: >-
${{ runner.os }}-${{ runner.arch }}-${{ steps.python.outputs.python-version }}-${{
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{
needs.info.outputs.python_cache_key }}
- name: Register Python problem matcher
run: |
+2 -2
View File
@@ -24,11 +24,11 @@ jobs:
uses: actions/checkout@v4.2.2
- name: Initialize CodeQL
uses: github/codeql-action/init@v3.28.17
uses: github/codeql-action/init@v3.28.16
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.28.17
uses: github/codeql-action/analyze@v3.28.16
with:
category: "/language:python"
+1 -1
View File
@@ -332,7 +332,6 @@ homeassistant.components.media_player.*
homeassistant.components.media_source.*
homeassistant.components.met_eireann.*
homeassistant.components.metoffice.*
homeassistant.components.miele.*
homeassistant.components.mikrotik.*
homeassistant.components.min_max.*
homeassistant.components.minecraft_server.*
@@ -434,6 +433,7 @@ homeassistant.components.roku.*
homeassistant.components.romy.*
homeassistant.components.rpi_power.*
homeassistant.components.rss_feed_template.*
homeassistant.components.rtsp_to_webrtc.*
homeassistant.components.russound_rio.*
homeassistant.components.ruuvi_gateway.*
homeassistant.components.ruuvitag_ble.*
Generated
+8 -8
View File
@@ -46,8 +46,8 @@ build.json @home-assistant/supervisor
/tests/components/accuweather/ @bieniu
/homeassistant/components/acmeda/ @atmurray
/tests/components/acmeda/ @atmurray
/homeassistant/components/adax/ @danielhiversen @lazytarget
/tests/components/adax/ @danielhiversen @lazytarget
/homeassistant/components/adax/ @danielhiversen
/tests/components/adax/ @danielhiversen
/homeassistant/components/adguard/ @frenck
/tests/components/adguard/ @frenck
/homeassistant/components/ads/ @mrpasztoradam
@@ -455,8 +455,8 @@ build.json @home-assistant/supervisor
/tests/components/evil_genius_labs/ @balloob
/homeassistant/components/evohome/ @zxdavb
/tests/components/evohome/ @zxdavb
/homeassistant/components/ezviz/ @RenierM26
/tests/components/ezviz/ @RenierM26
/homeassistant/components/ezviz/ @RenierM26 @baqs
/tests/components/ezviz/ @RenierM26 @baqs
/homeassistant/components/faa_delays/ @ntilley905
/tests/components/faa_delays/ @ntilley905
/homeassistant/components/fan/ @home-assistant/core
@@ -1111,8 +1111,8 @@ build.json @home-assistant/supervisor
/tests/components/opentherm_gw/ @mvn23
/homeassistant/components/openuv/ @bachya
/tests/components/openuv/ @bachya
/homeassistant/components/openweathermap/ @fabaff @freekode @nzapponi @wittypluck
/tests/components/openweathermap/ @fabaff @freekode @nzapponi @wittypluck
/homeassistant/components/openweathermap/ @fabaff @freekode @nzapponi
/tests/components/openweathermap/ @fabaff @freekode @nzapponi
/homeassistant/components/opnsense/ @mtreinish
/tests/components/opnsense/ @mtreinish
/homeassistant/components/opower/ @tronikos
@@ -1307,6 +1307,8 @@ build.json @home-assistant/supervisor
/tests/components/rpi_power/ @shenxn @swetoast
/homeassistant/components/rss_feed_template/ @home-assistant/core
/tests/components/rss_feed_template/ @home-assistant/core
/homeassistant/components/rtsp_to_webrtc/ @allenporter
/tests/components/rtsp_to_webrtc/ @allenporter
/homeassistant/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
/tests/components/ruckus_unleashed/ @lanrat @ms264556 @gabe565
/homeassistant/components/russound_rio/ @noahhusby
@@ -1794,8 +1796,6 @@ build.json @home-assistant/supervisor
/tests/components/zeversolar/ @kvanzuijlen
/homeassistant/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES
/tests/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES
/homeassistant/components/zimi/ @markhannon
/tests/components/zimi/ @markhannon
/homeassistant/components/zodiac/ @JulienTant
/tests/components/zodiac/ @JulienTant
/homeassistant/components/zone/ @home-assistant/core
+1 -1
View File
@@ -1,7 +1,7 @@
{
"domain": "adax",
"name": "Adax",
"codeowners": ["@danielhiversen", "@lazytarget"],
"codeowners": ["@danielhiversen"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/adax",
"iot_class": "local_polling",
@@ -8,7 +8,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AdvantageAirDataConfigEntry
from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN
from .const import ADVANTAGE_AIR_STATE_ON, DOMAIN as ADVANTAGE_AIR_DOMAIN
from .entity import AdvantageAirEntity, AdvantageAirThingEntity
from .models import AdvantageAirData
@@ -52,8 +52,8 @@ class AdvantageAirLight(AdvantageAirEntity, LightEntity):
self._id: str = light["id"]
self._attr_unique_id += f"-{self._id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._attr_unique_id)},
via_device=(DOMAIN, self.coordinator.data["system"]["rid"]),
identifiers={(ADVANTAGE_AIR_DOMAIN, self._attr_unique_id)},
via_device=(ADVANTAGE_AIR_DOMAIN, self.coordinator.data["system"]["rid"]),
manufacturer="Advantage Air",
model=light.get("moduleType"),
name=light["name"],
@@ -6,7 +6,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AdvantageAirDataConfigEntry
from .const import DOMAIN
from .const import DOMAIN as ADVANTAGE_AIR_DOMAIN
from .entity import AdvantageAirEntity
from .models import AdvantageAirData
@@ -32,7 +32,9 @@ class AdvantageAirApp(AdvantageAirEntity, UpdateEntity):
"""Initialize the Advantage Air App."""
super().__init__(instance)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self.coordinator.data["system"]["rid"])},
identifiers={
(ADVANTAGE_AIR_DOMAIN, self.coordinator.data["system"]["rid"])
},
manufacturer="Advantage Air",
model=self.coordinator.data["system"]["sysType"],
name=self.coordinator.data["system"]["name"],
@@ -10,7 +10,7 @@ from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN, SERVER_URL
from .const import DOMAIN as AGENT_DOMAIN, SERVER_URL
ATTRIBUTION = "ispyconnect.com"
DEFAULT_BRAND = "Agent DVR by ispyconnect.com"
@@ -46,7 +46,7 @@ async def async_setup_entry(
device_registry.async_get_or_create(
config_entry_id=config_entry.entry_id,
identifiers={(DOMAIN, agent_client.unique)},
identifiers={(AGENT_DOMAIN, agent_client.unique)},
manufacturer="iSpyConnect",
name=f"Agent {agent_client.name}",
model="Agent DVR",
@@ -12,7 +12,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import AgentDVRConfigEntry
from .const import DOMAIN
from .const import DOMAIN as AGENT_DOMAIN
CONF_HOME_MODE_NAME = "home"
CONF_AWAY_MODE_NAME = "away"
@@ -47,7 +47,7 @@ class AgentBaseStation(AlarmControlPanelEntity):
self._client = client
self._attr_unique_id = f"{client.unique}_CP"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, client.unique)},
identifiers={(AGENT_DOMAIN, client.unique)},
name=f"{client.name} {CONST_ALARM_CONTROL_PANEL_NAME}",
manufacturer="Agent",
model=CONST_ALARM_CONTROL_PANEL_NAME,
@@ -3,19 +3,6 @@
"name": "Airthings",
"codeowners": ["@danielhiversen", "@LaStrada"],
"config_flow": true,
"dhcp": [
{
"hostname": "airthings-view"
},
{
"hostname": "airthings-hub",
"macaddress": "D0141190*"
},
{
"hostname": "airthings-hub",
"macaddress": "70B3D52A0*"
}
],
"documentation": "https://www.home-assistant.io/integrations/airthings",
"iot_class": "cloud_polling",
"loggers": ["airthings"],
@@ -14,7 +14,6 @@ from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS,
EntityCategory,
@@ -79,12 +78,6 @@ SENSORS: dict[str, SensorEntityDescription] = {
translation_key="light",
state_class=SensorStateClass.MEASUREMENT,
),
"lux": SensorEntityDescription(
key="lux",
device_class=SensorDeviceClass.ILLUMINANCE,
native_unit_of_measurement=LIGHT_LUX,
state_class=SensorStateClass.MEASUREMENT,
),
"virusRisk": SensorEntityDescription(
key="virusRisk",
translation_key="virus_risk",
@@ -2,7 +2,7 @@
"config": {
"step": {
"user": {
"title": "Choose AlarmDecoder protocol",
"title": "Choose AlarmDecoder Protocol",
"data": {
"protocol": "Protocol"
}
@@ -12,8 +12,8 @@
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]",
"device_baudrate": "Device baud rate",
"device_path": "Device path"
"device_baudrate": "Device Baud Rate",
"device_path": "Device Path"
},
"data_description": {
"host": "The hostname or IP address of the AlarmDecoder device that is connected to your alarm panel.",
@@ -44,36 +44,36 @@
"arm_settings": {
"title": "[%key:component::alarmdecoder::options::step::init::title%]",
"data": {
"auto_bypass": "Auto-bypass on arm",
"code_arm_required": "Code required for arming",
"alt_night_mode": "Alternative night mode"
"auto_bypass": "Auto Bypass on Arm",
"code_arm_required": "Code Required for Arming",
"alt_night_mode": "Alternative Night Mode"
}
},
"zone_select": {
"title": "[%key:component::alarmdecoder::options::step::init::title%]",
"description": "Enter the zone number you'd like to to add, edit, or remove.",
"data": {
"zone_number": "Zone number"
"zone_number": "Zone Number"
}
},
"zone_details": {
"title": "[%key:component::alarmdecoder::options::step::init::title%]",
"description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave 'Zone name' blank.",
"description": "Enter details for zone {zone_number}. To delete zone {zone_number}, leave Zone Name blank.",
"data": {
"zone_name": "Zone name",
"zone_type": "Zone type",
"zone_rfid": "RF serial",
"zone_loop": "RF loop",
"zone_relayaddr": "Relay address",
"zone_relaychan": "Relay channel"
"zone_name": "Zone Name",
"zone_type": "Zone Type",
"zone_rfid": "RF Serial",
"zone_loop": "RF Loop",
"zone_relayaddr": "Relay Address",
"zone_relaychan": "Relay Channel"
}
}
},
"error": {
"relay_inclusive": "'Relay address' and 'Relay channel' are codependent and must be included together.",
"relay_inclusive": "Relay Address and Relay Channel are codependent and must be included together.",
"int": "The field below must be an integer.",
"loop_rfid": "'RF loop' cannot be used without 'RF serial'.",
"loop_range": "'RF loop' must be an integer between 1 and 4."
"loop_rfid": "RF Loop cannot be used without RF Serial.",
"loop_range": "RF Loop must be an integer between 1 and 4."
}
},
"services": {
+1 -1
View File
@@ -18,7 +18,7 @@
},
"step": {
"validation": {
"title": "Two-factor authentication",
"title": "Two factor authentication",
"data": {
"verification_code": "Verification code"
},
@@ -4,8 +4,8 @@
"user": {
"description": "The inverter must be connected via an RS485 adaptor, please select serial port and the inverter's address as configured on the LCD panel",
"data": {
"port": "RS485 or USB-RS485 adaptor port",
"address": "Inverter address"
"port": "RS485 or USB-RS485 Adaptor Port",
"address": "Inverter Address"
}
}
},
@@ -16,7 +16,7 @@
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"no_serial_ports": "No com ports found. The integration needs a valid RS485 device to communicate."
"no_serial_ports": "No com ports found. Need a valid RS485 device to communicate."
}
},
"entity": {
+2 -2
View File
@@ -5,7 +5,7 @@
"step": {
"init": {
"title": "Set up two-factor authentication using TOTP",
"description": "To activate two-factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six-digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**."
"description": "To activate two factor authentication using time-based one-time passwords, scan the QR code with your authentication app. If you don't have one, we recommend either [Google Authenticator](https://support.google.com/accounts/answer/1066447) or [Authy](https://authy.com/).\n\n{qr_code}\n\nAfter scanning the code, enter the six digit code from your app to verify the setup. If you have problems scanning the QR code, do a manual setup with code **`{code}`**."
}
},
"error": {
@@ -13,7 +13,7 @@
}
},
"notify": {
"title": "Notify one-time password",
"title": "Notify One-Time Password",
"step": {
"init": {
"title": "Set up one-time password delivered by notify component",
+2 -2
View File
@@ -14,7 +14,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity, EntityDescription
from .const import DOMAIN
from .const import DOMAIN as AXIS_DOMAIN
if TYPE_CHECKING:
from .hub import AxisHub
@@ -61,7 +61,7 @@ class AxisEntity(Entity):
self.hub = hub
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, hub.unique_id)},
identifiers={(AXIS_DOMAIN, hub.unique_id)},
serial_number=hub.unique_id,
)
@@ -30,7 +30,6 @@ class BackupCoordinatorData:
"""Class to hold backup data."""
backup_manager_state: BackupManagerState
last_attempted_automatic_backup: datetime | None
last_successful_automatic_backup: datetime | None
next_scheduled_automatic_backup: datetime | None
@@ -71,7 +70,6 @@ class BackupDataUpdateCoordinator(DataUpdateCoordinator[BackupCoordinatorData]):
"""Update backup manager data."""
return BackupCoordinatorData(
self.backup_manager.state,
self.backup_manager.config.data.last_attempted_automatic_backup,
self.backup_manager.config.data.last_completed_automatic_backup,
self.backup_manager.config.data.schedule.next_automatic_backup,
)
@@ -46,12 +46,6 @@ BACKUP_MANAGER_DESCRIPTIONS = (
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda data: data.last_successful_automatic_backup,
),
BackupSensorEntityDescription(
key="last_attempted_automatic_backup",
translation_key="last_attempted_automatic_backup",
device_class=SensorDeviceClass.TIMESTAMP,
value_fn=lambda data: data.last_attempted_automatic_backup,
),
)
@@ -37,9 +37,6 @@
"next_scheduled_automatic_backup": {
"name": "Next scheduled automatic backup"
},
"last_attempted_automatic_backup": {
"name": "Last attempted automatic backup"
},
"last_successful_automatic_backup": {
"name": "Last successful automatic backup"
}
-6
View File
@@ -332,9 +332,6 @@ def decrypt_backup(
except (DecryptError, SecureTarError, tarfile.TarError) as err:
LOGGER.warning("Error decrypting backup: %s", err)
error = err
except Exception as err: # noqa: BLE001
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
error = err
else:
# Pad the output stream to the requested minimum size
padding = max(minimum_size - output_stream.tell(), 0)
@@ -420,9 +417,6 @@ def encrypt_backup(
except (EncryptError, SecureTarError, tarfile.TarError) as err:
LOGGER.warning("Error encrypting backup: %s", err)
error = err
except Exception as err: # noqa: BLE001
LOGGER.exception("Unexpected error when decrypting backup: %s", err)
error = err
else:
# Pad the output stream to the requested minimum size
padding = max(minimum_size - output_stream.tell(), 0)
+3 -3
View File
@@ -2,7 +2,7 @@
"config": {
"step": {
"user": {
"title": "Sign in with Blink account",
"title": "Sign-in with Blink account",
"data": {
"username": "[%key:common::config_flow::data::username%]",
"password": "[%key:common::config_flow::data::password%]"
@@ -30,7 +30,7 @@
"step": {
"simple_options": {
"data": {
"scan_interval": "Scan interval (seconds)"
"scan_interval": "Scan Interval (seconds)"
},
"title": "Blink options",
"description": "Configure Blink integration"
@@ -93,7 +93,7 @@
},
"config_entry_id": {
"name": "Integration ID",
"description": "The Blink integration ID."
"description": "The Blink Integration ID."
}
}
}
@@ -21,7 +21,6 @@ from .coordinator import (
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
PLATFORMS = [
Platform.BUTTON,
Platform.MEDIA_PLAYER,
]
@@ -1,128 +0,0 @@
"""Button entities for Bluesound."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING
from pyblu import Player
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.const import CONF_PORT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
format_mac,
)
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import BluesoundCoordinator
from .media_player import DEFAULT_PORT
from .utils import format_unique_id
if TYPE_CHECKING:
from . import BluesoundConfigEntry
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BluesoundConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Bluesound entry."""
async_add_entities(
BluesoundButton(
config_entry.runtime_data.coordinator,
config_entry.runtime_data.player,
config_entry.data[CONF_PORT],
description,
)
for description in BUTTON_DESCRIPTIONS
)
@dataclass(kw_only=True, frozen=True)
class BluesoundButtonEntityDescription(ButtonEntityDescription):
"""Description for Bluesound button entities."""
press_fn: Callable[[Player], Awaitable[None]]
async def clear_sleep_timer(player: Player) -> None:
"""Clear the sleep timer."""
sleep = -1
while sleep != 0:
sleep = await player.sleep_timer()
async def set_sleep_timer(player: Player) -> None:
"""Set the sleep timer."""
await player.sleep_timer()
BUTTON_DESCRIPTIONS = [
BluesoundButtonEntityDescription(
key="set_sleep_timer",
translation_key="set_sleep_timer",
entity_registry_enabled_default=False,
press_fn=set_sleep_timer,
),
BluesoundButtonEntityDescription(
key="clear_sleep_timer",
translation_key="clear_sleep_timer",
entity_registry_enabled_default=False,
press_fn=clear_sleep_timer,
),
]
class BluesoundButton(CoordinatorEntity[BluesoundCoordinator], ButtonEntity):
"""Base class for Bluesound buttons."""
_attr_has_entity_name = True
entity_description: BluesoundButtonEntityDescription
def __init__(
self,
coordinator: BluesoundCoordinator,
player: Player,
port: int,
description: BluesoundButtonEntityDescription,
) -> None:
"""Initialize the Bluesound button."""
super().__init__(coordinator)
sync_status = coordinator.data.sync_status
self.entity_description = description
self._player = player
self._attr_unique_id = (
f"{description.key}-{format_unique_id(sync_status.mac, port)}"
)
if port == DEFAULT_PORT:
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, format_mac(sync_status.mac))},
connections={(CONNECTION_NETWORK_MAC, format_mac(sync_status.mac))},
name=sync_status.name,
manufacturer=sync_status.brand,
model=sync_status.model_name,
model_id=sync_status.model,
)
else:
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, format_unique_id(sync_status.mac, port))},
name=sync_status.name,
manufacturer=sync_status.brand,
model=sync_status.model_name,
model_id=sync_status.model,
via_device=(DOMAIN, format_mac(sync_status.mac)),
)
async def async_press(self) -> None:
"""Handle the button press."""
await self.entity_description.press_fn(self._player)
@@ -22,11 +22,7 @@ from homeassistant.components.media_player import (
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import (
config_validation as cv,
entity_platform,
issue_registry as ir,
)
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.device_registry import (
CONNECTION_NETWORK_MAC,
DeviceInfo,
@@ -38,7 +34,7 @@ from homeassistant.helpers.dispatcher import (
)
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util, slugify
from homeassistant.util import dt as dt_util
from .const import ATTR_BLUESOUND_GROUP, ATTR_MASTER, DOMAIN
from .coordinator import BluesoundCoordinator
@@ -492,36 +488,10 @@ class BluesoundPlayer(CoordinatorEntity[BluesoundCoordinator], MediaPlayerEntity
async def async_increase_timer(self) -> int:
"""Increase sleep time on player."""
ir.async_create_issue(
self.hass,
DOMAIN,
f"deprecated_service_{SERVICE_SET_TIMER}",
is_fixable=False,
breaks_in_ha_version="2025.12.0",
issue_domain=DOMAIN,
severity=ir.IssueSeverity.WARNING,
translation_key="deprecated_service_set_sleep_timer",
translation_placeholders={
"name": slugify(self.sync_status.name),
},
)
return await self._player.sleep_timer()
async def async_clear_timer(self) -> None:
"""Clear sleep timer on player."""
ir.async_create_issue(
self.hass,
DOMAIN,
f"deprecated_service_{SERVICE_CLEAR_TIMER}",
is_fixable=False,
breaks_in_ha_version="2025.12.0",
issue_domain=DOMAIN,
severity=ir.IssueSeverity.WARNING,
translation_key="deprecated_service_clear_sleep_timer",
translation_placeholders={
"name": slugify(self.sync_status.name),
},
)
sleep = 1
while sleep > 0:
sleep = await self._player.sleep_timer()
@@ -26,16 +26,6 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
}
},
"issues": {
"deprecated_service_set_sleep_timer": {
"title": "Detected use of deprecated action bluesound.set_sleep_timer",
"description": "Use `button.{name}_set_sleep_timer` instead.\n\nPlease replace this action and adjust your automations and scripts."
},
"deprecated_service_clear_sleep_timer": {
"title": "Detected use of deprecated action bluesound.clear_sleep_timer",
"description": "Use `button.{name}_clear_sleep_timer` instead.\n\nPlease replace this action and adjust your automations and scripts."
}
},
"services": {
"join": {
"name": "Join",
@@ -81,15 +71,5 @@
}
}
}
},
"entity": {
"button": {
"set_sleep_timer": {
"name": "Set sleep timer"
},
"clear_sleep_timer": {
"name": "Clear sleep timer"
}
}
}
}
@@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DOMAIN, BMWConfigEntry
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
from .entity import BMWBaseEntity
if TYPE_CHECKING:
@@ -111,7 +111,7 @@ class BMWButton(BMWBaseEntity, ButtonEntity):
await self.entity_description.remote_function(self.vehicle)
except MyBMWAPIError as ex:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
@@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DOMAIN, BMWConfigEntry
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
from .coordinator import BMWDataUpdateCoordinator
from .entity import BMWBaseEntity
@@ -71,7 +71,7 @@ class BMWLock(BMWBaseEntity, LockEntity):
self._attr_is_locked = None
self.async_write_ha_state()
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
@@ -95,7 +95,7 @@ class BMWLock(BMWBaseEntity, LockEntity):
self._attr_is_locked = None
self.async_write_ha_state()
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
@@ -20,7 +20,7 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN, BMWConfigEntry
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
PARALLEL_UPDATES = 1
@@ -92,7 +92,7 @@ class BMWNotificationService(BaseNotificationService):
except (vol.Invalid, TypeError, ValueError) as ex:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="invalid_poi",
translation_placeholders={
"poi_exception": str(ex),
@@ -107,7 +107,7 @@ class BMWNotificationService(BaseNotificationService):
await vehicle.remote_services.trigger_send_poi(poi)
except MyBMWAPIError as ex:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
@@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DOMAIN, BMWConfigEntry
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
from .coordinator import BMWDataUpdateCoordinator
from .entity import BMWBaseEntity
@@ -110,7 +110,7 @@ class BMWNumber(BMWBaseEntity, NumberEntity):
await self.entity_description.remote_service(self.vehicle, value)
except MyBMWAPIError as ex:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
@@ -15,7 +15,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DOMAIN, BMWConfigEntry
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
from .coordinator import BMWDataUpdateCoordinator
from .entity import BMWBaseEntity
@@ -124,7 +124,7 @@ class BMWSelect(BMWBaseEntity, SelectEntity):
await self.entity_description.remote_service(self.vehicle, option)
except MyBMWAPIError as ex:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
@@ -69,7 +69,7 @@
"name": "Door lock state"
},
"condition_based_services": {
"name": "Condition-based services"
"name": "Condition based services"
},
"check_control_messages": {
"name": "Check control messages"
@@ -81,7 +81,7 @@
"name": "Connection status"
},
"is_pre_entry_climatization_enabled": {
"name": "Pre-entry climatization"
"name": "Pre entry climatization"
}
},
"button": {
@@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DOMAIN, BMWConfigEntry
from . import DOMAIN as BMW_DOMAIN, BMWConfigEntry
from .coordinator import BMWDataUpdateCoordinator
from .entity import BMWBaseEntity
@@ -112,7 +112,7 @@ class BMWSwitch(BMWBaseEntity, SwitchEntity):
await self.entity_description.remote_service_on(self.vehicle)
except MyBMWAPIError as ex:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
@@ -124,7 +124,7 @@ class BMWSwitch(BMWBaseEntity, SwitchEntity):
await self.entity_description.remote_service_off(self.vehicle)
except MyBMWAPIError as ex:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_domain=BMW_DOMAIN,
translation_key="remote_service_error",
translation_placeholders={"exception": str(ex)},
) from ex
+1 -2
View File
@@ -5,7 +5,7 @@ import logging
from typing import Any
from aiohttp import ClientError, ClientResponseError, ClientTimeout
from bond_async import Bond, BPUPSubscriptions, RequestorUUID, start_bpup
from bond_async import Bond, BPUPSubscriptions, start_bpup
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
@@ -49,7 +49,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: BondConfigEntry) -> bool
token=token,
timeout=ClientTimeout(total=_API_TIMEOUT),
session=async_get_clientsession(hass),
requestor_uuid=RequestorUUID.HOME_ASSISTANT,
)
hub = BondHub(bond, host)
try:
+3 -11
View File
@@ -8,7 +8,7 @@ import logging
from typing import Any
from aiohttp import ClientConnectionError, ClientResponseError
from bond_async import Bond, RequestorUUID
from bond_async import Bond
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState, ConfigFlow, ConfigFlowResult
@@ -34,12 +34,7 @@ TOKEN_SCHEMA = vol.Schema({})
async def async_get_token(hass: HomeAssistant, host: str) -> str | None:
"""Try to fetch the token from the bond device."""
bond = Bond(
host,
"",
session=async_get_clientsession(hass),
requestor_uuid=RequestorUUID.HOME_ASSISTANT,
)
bond = Bond(host, "", session=async_get_clientsession(hass))
response: dict[str, str] = {}
with contextlib.suppress(ClientConnectionError):
response = await bond.token()
@@ -50,10 +45,7 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> tuple[st
"""Validate the user input allows us to connect."""
bond = Bond(
data[CONF_HOST],
data[CONF_ACCESS_TOKEN],
session=async_get_clientsession(hass),
requestor_uuid=RequestorUUID.HOME_ASSISTANT,
data[CONF_HOST], data[CONF_ACCESS_TOKEN], session=async_get_clientsession(hass)
)
try:
hub = BondHub(bond, data[CONF_HOST])
@@ -14,11 +14,7 @@ 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,
Platform.SENSOR,
Platform.SWITCH,
]
PLATFORMS: list[Platform] = [Platform.ALARM_CONTROL_PANEL, Platform.SENSOR]
type BoschAlarmConfigEntry = ConfigEntry[Panel]
@@ -86,57 +86,3 @@ class BoschAlarmAreaEntity(BoschAlarmEntity):
self._area.ready_observer.detach(self.schedule_update_ha_state)
if self._observe_status:
self._area.status_observer.detach(self.schedule_update_ha_state)
class BoschAlarmDoorEntity(BoschAlarmEntity):
"""A base entity for area related entities within a bosch alarm panel."""
def __init__(self, panel: Panel, door_id: int, unique_id: str) -> None:
"""Set up a area related entity for a bosch alarm panel."""
super().__init__(panel, unique_id)
self._door_id = door_id
self._door = panel.doors[door_id]
self._door_unique_id = f"{unique_id}_door_{door_id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._door_unique_id)},
name=self._door.name,
manufacturer="Bosch Security Systems",
via_device=(DOMAIN, unique_id),
)
async def async_added_to_hass(self) -> None:
"""Observe state changes."""
await super().async_added_to_hass()
self._door.status_observer.attach(self.schedule_update_ha_state)
async def async_will_remove_from_hass(self) -> None:
"""Stop observing state changes."""
await super().async_added_to_hass()
self._door.status_observer.detach(self.schedule_update_ha_state)
class BoschAlarmOutputEntity(BoschAlarmEntity):
"""A base entity for area related entities within a bosch alarm panel."""
def __init__(self, panel: Panel, output_id: int, unique_id: str) -> None:
"""Set up a output related entity for a bosch alarm panel."""
super().__init__(panel, unique_id)
self._output_id = output_id
self._output = panel.outputs[output_id]
self._output_unique_id = f"{unique_id}_output_{output_id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._output_unique_id)},
name=self._output.name,
manufacturer="Bosch Security Systems",
via_device=(DOMAIN, unique_id),
)
async def async_added_to_hass(self) -> None:
"""Observe state changes."""
await super().async_added_to_hass()
self._output.status_observer.attach(self.schedule_update_ha_state)
async def async_will_remove_from_hass(self) -> None:
"""Stop observing state changes."""
await super().async_added_to_hass()
self._output.status_observer.detach(self.schedule_update_ha_state)
@@ -2,27 +2,7 @@
"entity": {
"sensor": {
"faulting_points": {
"default": "mdi:alert-circle"
}
},
"switch": {
"locked": {
"default": "mdi:lock",
"state": {
"off": "mdi:lock-open"
}
},
"secured": {
"default": "mdi:lock",
"state": {
"off": "mdi:lock-open"
}
},
"cycling": {
"default": "mdi:lock",
"state": {
"on": "mdi:lock-open"
}
"default": "mdi:alert-circle-outline"
}
}
}
@@ -54,23 +54,9 @@
},
"authentication_failed": {
"message": "Incorrect credentials for panel."
},
"incorrect_door_state": {
"message": "Door cannot be manipulated while it is being cycled."
}
},
"entity": {
"switch": {
"secured": {
"name": "Secured"
},
"cycling": {
"name": "Cycling"
},
"locked": {
"name": "Locked"
}
},
"sensor": {
"faulting_points": {
"name": "Faulting points",
@@ -1,150 +0,0 @@
"""Support for Bosch Alarm Panel outputs and doors as switches."""
from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from typing import Any
from bosch_alarm_mode2 import Panel
from bosch_alarm_mode2.panel import Door
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BoschAlarmConfigEntry
from .const import DOMAIN
from .entity import BoschAlarmDoorEntity, BoschAlarmOutputEntity
@dataclass(kw_only=True, frozen=True)
class BoschAlarmSwitchEntityDescription(SwitchEntityDescription):
"""Describes Bosch Alarm door entity."""
value_fn: Callable[[Door], bool]
on_fn: Callable[[Panel, int], Coroutine[Any, Any, None]]
off_fn: Callable[[Panel, int], Coroutine[Any, Any, None]]
DOOR_SWITCH_TYPES: list[BoschAlarmSwitchEntityDescription] = [
BoschAlarmSwitchEntityDescription(
key="locked",
translation_key="locked",
value_fn=lambda door: door.is_locked(),
on_fn=lambda panel, door_id: panel.door_relock(door_id),
off_fn=lambda panel, door_id: panel.door_unlock(door_id),
),
BoschAlarmSwitchEntityDescription(
key="secured",
translation_key="secured",
value_fn=lambda door: door.is_secured(),
on_fn=lambda panel, door_id: panel.door_secure(door_id),
off_fn=lambda panel, door_id: panel.door_unsecure(door_id),
),
BoschAlarmSwitchEntityDescription(
key="cycling",
translation_key="cycling",
value_fn=lambda door: door.is_cycling(),
on_fn=lambda panel, door_id: panel.door_cycle(door_id),
off_fn=lambda panel, door_id: panel.door_relock(door_id),
),
]
async def async_setup_entry(
hass: HomeAssistant,
config_entry: BoschAlarmConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up switch entities for outputs."""
panel = config_entry.runtime_data
entities: list[SwitchEntity] = [
PanelOutputEntity(
panel, output_id, config_entry.unique_id or config_entry.entry_id
)
for output_id in panel.outputs
]
entities.extend(
PanelDoorEntity(
panel,
door_id,
config_entry.unique_id or config_entry.entry_id,
entity_description,
)
for door_id in panel.doors
for entity_description in DOOR_SWITCH_TYPES
)
async_add_entities(entities)
PARALLEL_UPDATES = 0
class PanelDoorEntity(BoschAlarmDoorEntity, SwitchEntity):
"""A switch entity for a door on a bosch alarm panel."""
entity_description: BoschAlarmSwitchEntityDescription
def __init__(
self,
panel: Panel,
door_id: int,
unique_id: str,
entity_description: BoschAlarmSwitchEntityDescription,
) -> None:
"""Set up a switch entity for a door on a bosch alarm panel."""
super().__init__(panel, door_id, unique_id)
self.entity_description = entity_description
self._attr_unique_id = f"{self._door_unique_id}_{entity_description.key}"
@property
def is_on(self) -> bool:
"""Return the value function."""
return self.entity_description.value_fn(self._door)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Run the on function."""
# If the door is currently cycling, we can't send it any other commands until it is done
if self._door.is_cycling():
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="incorrect_door_state"
)
await self.entity_description.on_fn(self.panel, self._door_id)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Run the off function."""
# If the door is currently cycling, we can't send it any other commands until it is done
if self._door.is_cycling():
raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="incorrect_door_state"
)
await self.entity_description.off_fn(self.panel, self._door_id)
class PanelOutputEntity(BoschAlarmOutputEntity, SwitchEntity):
"""An output entity for a bosch alarm panel."""
_attr_name = None
def __init__(self, panel: Panel, output_id: int, unique_id: str) -> None:
"""Set up an output entity for a bosch alarm panel."""
super().__init__(panel, output_id, unique_id)
self._attr_unique_id = self._output_unique_id
@property
def is_on(self) -> bool:
"""Check if this entity is on."""
return self._output.is_active()
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on this output."""
await self.panel.set_output_active(self._output_id)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off this output."""
await self.panel.set_output_inactive(self._output_id)
+2 -10
View File
@@ -10,12 +10,7 @@ from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .coordinator import (
BringActivityCoordinator,
BringConfigEntry,
BringCoordinators,
BringDataUpdateCoordinator,
)
from .coordinator import BringConfigEntry, BringDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.EVENT, Platform.SENSOR, Platform.TODO]
@@ -31,10 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: BringConfigEntry) -> boo
coordinator = BringDataUpdateCoordinator(hass, entry, bring)
await coordinator.async_config_entry_first_refresh()
activity_coordinator = BringActivityCoordinator(hass, entry, coordinator)
await activity_coordinator.async_config_entry_first_refresh()
entry.runtime_data = BringCoordinators(coordinator, activity_coordinator)
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
+8 -87
View File
@@ -30,15 +30,7 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
type BringConfigEntry = ConfigEntry[BringCoordinators]
@dataclass
class BringCoordinators:
"""Data class holding coordinators."""
data: BringDataUpdateCoordinator
activity: BringActivityCoordinator
type BringConfigEntry = ConfigEntry[BringDataUpdateCoordinator]
@dataclass(frozen=True)
@@ -47,27 +39,16 @@ class BringData(DataClassORJSONMixin):
lst: BringList
content: BringItemsResponse
@dataclass(frozen=True)
class BringActivityData(DataClassORJSONMixin):
"""Coordinator data class."""
activity: BringActivityResponse
users: BringUsersResponse
class BringBaseCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
"""Bring base coordinator."""
config_entry: BringConfigEntry
lists: list[BringList]
class BringDataUpdateCoordinator(BringBaseCoordinator[dict[str, BringData]]):
class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]):
"""A Bring Data Update Coordinator."""
config_entry: BringConfigEntry
user_settings: BringUserSettingsResponse
lists: list[BringList]
def __init__(
self, hass: HomeAssistant, config_entry: BringConfigEntry, bring: Bring
@@ -109,19 +90,16 @@ class BringDataUpdateCoordinator(BringBaseCoordinator[dict[str, BringData]]):
current_lists := {lst.listUuid for lst in self.lists}
):
self._purge_deleted_lists()
new_lists = current_lists - self.previous_lists
self.previous_lists = current_lists
list_dict: dict[str, BringData] = {}
for lst in self.lists:
if (
(ctx := set(self.async_contexts()))
and lst.listUuid not in ctx
and lst.listUuid not in new_lists
):
if (ctx := set(self.async_contexts())) and lst.listUuid not in ctx:
continue
try:
items = await self.bring.get_list(lst.listUuid)
activity = await self.bring.get_activity(lst.listUuid)
users = await self.bring.get_list_users(lst.listUuid)
except BringRequestException as e:
raise UpdateFailed(
translation_domain=DOMAIN,
@@ -133,7 +111,7 @@ class BringDataUpdateCoordinator(BringBaseCoordinator[dict[str, BringData]]):
translation_key="setup_parse_exception",
) from e
else:
list_dict[lst.listUuid] = BringData(lst, items)
list_dict[lst.listUuid] = BringData(lst, items, activity, users)
return list_dict
@@ -178,60 +156,3 @@ class BringDataUpdateCoordinator(BringBaseCoordinator[dict[str, BringData]]):
device_reg.async_update_device(
device.id, remove_config_entry_id=self.config_entry.entry_id
)
class BringActivityCoordinator(BringBaseCoordinator[dict[str, BringActivityData]]):
"""A Bring Activity Data Update Coordinator."""
user_settings: BringUserSettingsResponse
def __init__(
self,
hass: HomeAssistant,
config_entry: BringConfigEntry,
coordinator: BringDataUpdateCoordinator,
) -> None:
"""Initialize the Bring Activity data coordinator."""
super().__init__(
hass,
_LOGGER,
config_entry=config_entry,
name=DOMAIN,
update_interval=timedelta(minutes=10),
)
self.coordinator = coordinator
self.lists = coordinator.lists
async def _async_update_data(self) -> dict[str, BringActivityData]:
"""Fetch activity data from bring."""
list_dict: dict[str, BringActivityData] = {}
for lst in self.lists:
if (
ctx := set(self.coordinator.async_contexts())
) and lst.listUuid not in ctx:
continue
try:
activity = await self.coordinator.bring.get_activity(lst.listUuid)
users = await self.coordinator.bring.get_list_users(lst.listUuid)
except BringAuthException as e:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="setup_authentication_exception",
translation_placeholders={CONF_EMAIL: self.coordinator.bring.mail},
) from e
except BringRequestException as e:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="setup_request_exception",
) from e
except BringParseException as e:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="setup_parse_exception",
) from e
else:
list_dict[lst.listUuid] = BringActivityData(activity, users)
return list_dict
@@ -20,12 +20,9 @@ async def async_get_config_entry_diagnostics(
return {
"data": {
k: v.to_dict() for k, v in config_entry.runtime_data.data.data.items()
},
"activity": {
k: async_redact_data(v.to_dict(), TO_REDACT)
for k, v in config_entry.runtime_data.activity.data.items()
for k, v in config_entry.runtime_data.data.items()
},
"lists": [lst.to_dict() for lst in config_entry.runtime_data.data.lists],
"user_settings": config_entry.runtime_data.data.user_settings.to_dict(),
"lists": [lst.to_dict() for lst in config_entry.runtime_data.lists],
"user_settings": config_entry.runtime_data.user_settings.to_dict(),
}
+4 -6
View File
@@ -8,17 +8,17 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import BringBaseCoordinator
from .coordinator import BringDataUpdateCoordinator
class BringBaseEntity(CoordinatorEntity[BringBaseCoordinator]):
class BringBaseEntity(CoordinatorEntity[BringDataUpdateCoordinator]):
"""Bring base entity."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: BringBaseCoordinator,
coordinator: BringDataUpdateCoordinator,
bring_list: BringList,
) -> None:
"""Initialize the entity."""
@@ -34,7 +34,5 @@ class BringBaseEntity(CoordinatorEntity[BringBaseCoordinator]):
},
manufacturer="Bring! Labs AG",
model="Bring! Grocery Shopping List",
configuration_url=f"https://web.getbring.com/app/lists/{list(self.coordinator.lists).index(bring_list)}"
if bring_list in self.coordinator.lists
else None,
configuration_url=f"https://web.getbring.com/app/lists/{list(self.coordinator.lists).index(bring_list)}",
)
+6 -7
View File
@@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import BringConfigEntry
from .coordinator import BringActivityCoordinator
from .coordinator import BringDataUpdateCoordinator
from .entity import BringBaseEntity
PARALLEL_UPDATES = 0
@@ -32,18 +32,18 @@ async def async_setup_entry(
"""Add event entities."""
nonlocal lists_added
if new_lists := {lst.listUuid for lst in coordinator.data.lists} - lists_added:
if new_lists := {lst.listUuid for lst in coordinator.lists} - lists_added:
async_add_entities(
BringEventEntity(
coordinator.activity,
coordinator,
bring_list,
)
for bring_list in coordinator.data.lists
for bring_list in coordinator.lists
if bring_list.listUuid in new_lists
)
lists_added |= new_lists
coordinator.activity.async_add_listener(add_entities)
coordinator.async_add_listener(add_entities)
add_entities()
@@ -51,11 +51,10 @@ class BringEventEntity(BringBaseEntity, EventEntity):
"""An event entity."""
_attr_translation_key = "activities"
coordinator: BringActivityCoordinator
def __init__(
self,
coordinator: BringActivityCoordinator,
coordinator: BringDataUpdateCoordinator,
bring_list: BringList,
) -> None:
"""Initialize the entity."""
+1 -2
View File
@@ -88,7 +88,7 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the sensor platform."""
coordinator = config_entry.runtime_data.data
coordinator = config_entry.runtime_data
lists_added: set[str] = set()
@callback
@@ -117,7 +117,6 @@ class BringSensorEntity(BringBaseEntity, SensorEntity):
"""A sensor entity."""
entity_description: BringSensorEntityDescription
coordinator: BringDataUpdateCoordinator
def __init__(
self,
+2 -5
View File
@@ -44,7 +44,7 @@ async def async_setup_entry(
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the sensor from a config entry created in the integrations UI."""
coordinator = config_entry.runtime_data.data
coordinator = config_entry.runtime_data
lists_added: set[str] = set()
@callback
@@ -88,7 +88,6 @@ class BringTodoListEntity(BringBaseEntity, TodoListEntity):
| TodoListEntityFeature.DELETE_TODO_ITEM
| TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM
)
coordinator: BringDataUpdateCoordinator
def __init__(
self, coordinator: BringDataUpdateCoordinator, bring_list: BringList
@@ -108,9 +107,7 @@ class BringTodoListEntity(BringBaseEntity, TodoListEntity):
description=item.specification,
status=TodoItemStatus.NEEDS_ACTION,
)
for item in sorted(
self.bring_list.content.items.purchase, key=lambda i: i.itemId
)
for item in self.bring_list.content.items.purchase
),
*(
TodoItem(
@@ -11,13 +11,6 @@
},
"audio_output": {
"default": "mdi:audio-input-stereo-minijack"
},
"control_bus_mode": {
"default": "mdi:audio-video-off",
"state": {
"amplifier": "mdi:speaker",
"receiver": "mdi:audio-video"
}
}
},
"switch": {
@@ -11,7 +11,6 @@ from aiostreammagic import (
StreamMagicClient,
TransportControl,
)
from aiostreammagic.models import ControlBusMode
from homeassistant.components.media_player import (
BrowseMedia,
@@ -92,8 +91,6 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
features = BASE_FEATURES
if self.client.state.pre_amp_mode:
features |= PREAMP_FEATURES
if self.client.state.control_bus == ControlBusMode.AMPLIFIER:
features |= MediaPlayerEntityFeature.VOLUME_STEP
if TransportControl.PLAY_PAUSE in controls:
features |= MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PAUSE
for control in controls:
@@ -4,7 +4,7 @@ from collections.abc import Awaitable, Callable
from dataclasses import dataclass, field
from aiostreammagic import StreamMagicClient
from aiostreammagic.models import ControlBusMode, DisplayBrightness
from aiostreammagic.models import DisplayBrightness
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory
@@ -76,20 +76,6 @@ CONTROL_ENTITIES: tuple[CambridgeAudioSelectEntityDescription, ...] = (
value_fn=_audio_output_value_fn,
set_value_fn=_audio_output_set_value_fn,
),
CambridgeAudioSelectEntityDescription(
key="control_bus_mode",
translation_key="control_bus_mode",
options=[
ControlBusMode.AMPLIFIER.value,
ControlBusMode.RECEIVER.value,
ControlBusMode.OFF.value,
],
entity_category=EntityCategory.CONFIG,
value_fn=lambda client: client.state.control_bus,
set_value_fn=lambda client, value: client.set_control_bus_mode(
ControlBusMode(value)
),
),
)
@@ -46,14 +46,6 @@
},
"audio_output": {
"name": "Audio output"
},
"control_bus_mode": {
"name": "Control Bus mode",
"state": {
"amplifier": "Amplifier",
"receiver": "Receiver",
"off": "[%key:common::state::off%]"
}
}
},
"switch": {
+78 -4
View File
@@ -61,6 +61,7 @@ from homeassistant.helpers.deprecation import (
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.frame import ReportBehavior, report_usage
from homeassistant.helpers.network import get_url
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, VolDictType
@@ -85,6 +86,7 @@ from .img_util import scale_jpeg_camera_image
from .prefs import CameraPreferences, DynamicStreamSettings # noqa: F401
from .webrtc import (
DATA_ICE_SERVERS,
CameraWebRTCLegacyProvider,
CameraWebRTCProvider,
WebRTCAnswer,
WebRTCCandidate, # noqa: F401
@@ -92,8 +94,10 @@ from .webrtc import (
WebRTCError,
WebRTCMessage, # noqa: F401
WebRTCSendMessage,
async_get_supported_legacy_provider,
async_get_supported_provider,
async_register_ice_servers,
async_register_rtsp_to_web_rtc_provider, # noqa: F401
async_register_webrtc_provider, # noqa: F401
async_register_ws,
)
@@ -432,6 +436,7 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
CACHED_PROPERTIES_WITH_ATTR_ = {
"brand",
"frame_interval",
"frontend_stream_type",
"is_on",
"is_recording",
"is_streaming",
@@ -451,6 +456,8 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
# Entity Properties
_attr_brand: str | None = None
_attr_frame_interval: float = MIN_STREAM_INTERVAL
# Deprecated in 2024.12. Remove in 2025.6
_attr_frontend_stream_type: StreamType | None
_attr_is_on: bool = True
_attr_is_recording: bool = False
_attr_is_streaming: bool = False
@@ -473,6 +480,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
self.async_update_token()
self._create_stream_lock: asyncio.Lock | None = None
self._webrtc_provider: CameraWebRTCProvider | None = None
self._legacy_webrtc_provider: CameraWebRTCLegacyProvider | None = None
self._supports_native_sync_webrtc = (
type(self).async_handle_web_rtc_offer != Camera.async_handle_web_rtc_offer
)
@@ -480,6 +488,16 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
type(self).async_handle_async_webrtc_offer
!= Camera.async_handle_async_webrtc_offer
)
self._deprecate_attr_frontend_stream_type_logged = False
if type(self).frontend_stream_type != Camera.frontend_stream_type:
report_usage(
(
f"is overwriting the 'frontend_stream_type' property in the {type(self).__name__} class,"
" which is deprecated and will be removed in Home Assistant 2025.6, "
),
core_integration_behavior=ReportBehavior.ERROR,
exclude_integrations={DOMAIN},
)
@cached_property
def entity_picture(self) -> str:
@@ -541,6 +559,40 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
"""Return the interval between frames of the mjpeg stream."""
return self._attr_frame_interval
@property
def frontend_stream_type(self) -> StreamType | None:
"""Return the type of stream supported by this camera.
A camera may have a single stream type which is used to inform the
frontend which camera attributes and player to use. The default type
is to use HLS, and components can override to change the type.
"""
# Deprecated in 2024.12. Remove in 2025.6
# Use the camera_capabilities instead
if hasattr(self, "_attr_frontend_stream_type"):
if not self._deprecate_attr_frontend_stream_type_logged:
report_usage(
(
f"is setting the '_attr_frontend_stream_type' attribute in the {type(self).__name__} class,"
" which is deprecated and will be removed in Home Assistant 2025.6, "
),
core_integration_behavior=ReportBehavior.ERROR,
exclude_integrations={DOMAIN},
)
self._deprecate_attr_frontend_stream_type_logged = True
return self._attr_frontend_stream_type
if CameraEntityFeature.STREAM not in self.supported_features_compat:
return None
if (
self._webrtc_provider
or self._legacy_webrtc_provider
or self._supports_native_sync_webrtc
or self._supports_native_async_webrtc
):
return StreamType.WEB_RTC
return StreamType.HLS
@property
def available(self) -> bool:
"""Return True if entity is available."""
@@ -642,7 +694,14 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
)
return
raise HomeAssistantError("Camera does not support WebRTC")
if self._legacy_webrtc_provider and (
answer := await self._legacy_webrtc_provider.async_handle_web_rtc_offer(
self, offer_sdp
)
):
send_message(WebRTCAnswer(answer))
else:
raise HomeAssistantError("Camera does not support WebRTC")
def camera_image(
self, width: int | None = None, height: int | None = None
@@ -738,6 +797,9 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
if motion_detection_enabled := self.motion_detection_enabled:
attrs["motion_detection"] = motion_detection_enabled
if frontend_stream_type := self.frontend_stream_type:
attrs["frontend_stream_type"] = frontend_stream_type
return attrs
@callback
@@ -761,7 +823,9 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
providers or inputs to the state attributes change.
"""
old_provider = self._webrtc_provider
old_legacy_provider = self._legacy_webrtc_provider
new_provider = None
new_legacy_provider = None
# Skip all providers if the camera has a native WebRTC implementation
if not (
@@ -772,8 +836,15 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
async_get_supported_provider
)
if old_provider != new_provider:
if new_provider is None:
# Only add the legacy provider if the new provider is not available
new_legacy_provider = await self._async_get_supported_webrtc_provider(
async_get_supported_legacy_provider
)
if old_provider != new_provider or old_legacy_provider != new_legacy_provider:
self._webrtc_provider = new_provider
self._legacy_webrtc_provider = new_legacy_provider
self._invalidate_camera_capabilities_cache()
if write_state:
self.async_write_ha_state()
@@ -808,7 +879,10 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
]
config.configuration.ice_servers.extend(ice_servers)
config.get_candidates_upfront = self._supports_native_sync_webrtc
config.get_candidates_upfront = (
self._supports_native_sync_webrtc
or self._legacy_webrtc_provider is not None
)
return config
@@ -844,7 +918,7 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
else:
frontend_stream_types.add(StreamType.HLS)
if self._webrtc_provider:
if self._webrtc_provider or self._legacy_webrtc_provider:
frontend_stream_types.add(StreamType.WEB_RTC)
return CameraCapabilities(frontend_stream_types)
@@ -46,6 +46,10 @@
}
}
}
},
"legacy_webrtc_provider": {
"title": "Detected use of legacy WebRTC provider registered by {legacy_integration}",
"description": "The {legacy_integration} integration has registered a legacy WebRTC provider. Home Assistant prefers using the built-in modern WebRTC provider registered by the {builtin_integration} integration.\n\nBenefits of the built-in integration are:\n\n- The camera stream is started faster.\n- More camera devices are supported.\n\nTo fix this issue, you can either keep using the built-in modern WebRTC provider and remove the {legacy_integration} integration or remove the {builtin_integration} integration to use the legacy provider, and then restart Home Assistant."
}
},
"services": {
+126 -2
View File
@@ -8,7 +8,7 @@ from collections.abc import Awaitable, Callable, Iterable
from dataclasses import asdict, dataclass, field
from functools import cache, partial, wraps
import logging
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Protocol
from mashumaro import MissingField
import voluptuous as vol
@@ -22,7 +22,8 @@ from webrtc_models import (
from homeassistant.components import websocket_api
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import config_validation as cv, issue_registry as ir
from homeassistant.helpers.deprecation import deprecated_function
from homeassistant.util.hass_dict import HassKey
from homeassistant.util.ulid import ulid
@@ -38,6 +39,9 @@ _LOGGER = logging.getLogger(__name__)
DATA_WEBRTC_PROVIDERS: HassKey[set[CameraWebRTCProvider]] = HassKey(
"camera_webrtc_providers"
)
DATA_WEBRTC_LEGACY_PROVIDERS: HassKey[dict[str, CameraWebRTCLegacyProvider]] = HassKey(
"camera_webrtc_legacy_providers"
)
DATA_ICE_SERVERS: HassKey[list[Callable[[], Iterable[RTCIceServer]]]] = HassKey(
"camera_webrtc_ice_servers"
)
@@ -159,6 +163,18 @@ class CameraWebRTCProvider(ABC):
return ## This is an optional method so we need a default here.
class CameraWebRTCLegacyProvider(Protocol):
"""WebRTC provider."""
async def async_is_supported(self, stream_source: str) -> bool:
"""Determine if the provider supports the stream source."""
async def async_handle_web_rtc_offer(
self, camera: Camera, offer_sdp: str
) -> str | None:
"""Handle the WebRTC offer and return an answer."""
@callback
def async_register_webrtc_provider(
hass: HomeAssistant,
@@ -188,6 +204,8 @@ def async_register_webrtc_provider(
async def _async_refresh_providers(hass: HomeAssistant) -> None:
"""Check all cameras for any state changes for registered providers."""
_async_check_conflicting_legacy_provider(hass)
component = hass.data[DATA_COMPONENT]
await asyncio.gather(
*(camera.async_refresh_providers() for camera in component.entities)
@@ -362,6 +380,21 @@ async def async_get_supported_provider(
return None
async def async_get_supported_legacy_provider(
hass: HomeAssistant, camera: Camera
) -> CameraWebRTCLegacyProvider | None:
"""Return the first supported provider for the camera."""
providers = hass.data.get(DATA_WEBRTC_LEGACY_PROVIDERS)
if not providers or not (stream_source := await camera.stream_source()):
return None
for provider in providers.values():
if await provider.async_is_supported(stream_source):
return provider
return None
@callback
def async_register_ice_servers(
hass: HomeAssistant,
@@ -378,3 +411,94 @@ def async_register_ice_servers(
servers.append(get_ice_server_fn)
return remove
# The following code is legacy code that was introduced with rtsp_to_webrtc and will be deprecated/removed in the future.
# Left it so custom integrations can still use it.
_RTSP_PREFIXES = {"rtsp://", "rtsps://", "rtmp://"}
# An RtspToWebRtcProvider accepts these inputs:
# stream_source: The RTSP url
# offer_sdp: The WebRTC SDP offer
# stream_id: A unique id for the stream, used to update an existing source
# The output is the SDP answer, or None if the source or offer is not eligible.
# The Callable may throw HomeAssistantError on failure.
type RtspToWebRtcProviderType = Callable[[str, str, str], Awaitable[str | None]]
class _CameraRtspToWebRTCProvider(CameraWebRTCLegacyProvider):
def __init__(self, fn: RtspToWebRtcProviderType) -> None:
"""Initialize the RTSP to WebRTC provider."""
self._fn = fn
async def async_is_supported(self, stream_source: str) -> bool:
"""Return if this provider is supports the Camera as source."""
return any(stream_source.startswith(prefix) for prefix in _RTSP_PREFIXES)
async def async_handle_web_rtc_offer(
self, camera: Camera, offer_sdp: str
) -> str | None:
"""Handle the WebRTC offer and return an answer."""
if not (stream_source := await camera.stream_source()):
return None
return await self._fn(stream_source, offer_sdp, camera.entity_id)
@deprecated_function("async_register_webrtc_provider", breaks_in_ha_version="2025.6")
def async_register_rtsp_to_web_rtc_provider(
hass: HomeAssistant,
domain: str,
provider: RtspToWebRtcProviderType,
) -> Callable[[], None]:
"""Register an RTSP to WebRTC provider.
The first provider to satisfy the offer will be used.
"""
if DOMAIN not in hass.data:
raise ValueError("Unexpected state, camera not loaded")
legacy_providers = hass.data.setdefault(DATA_WEBRTC_LEGACY_PROVIDERS, {})
if domain in legacy_providers:
raise ValueError("Provider already registered")
provider_instance = _CameraRtspToWebRTCProvider(provider)
@callback
def remove_provider() -> None:
legacy_providers.pop(domain)
hass.async_create_task(_async_refresh_providers(hass))
legacy_providers[domain] = provider_instance
hass.async_create_task(_async_refresh_providers(hass))
return remove_provider
@callback
def _async_check_conflicting_legacy_provider(hass: HomeAssistant) -> None:
"""Check if a legacy provider is registered together with the builtin provider."""
builtin_provider_domain = "go2rtc"
if (
(legacy_providers := hass.data.get(DATA_WEBRTC_LEGACY_PROVIDERS))
and (providers := hass.data.get(DATA_WEBRTC_PROVIDERS))
and any(provider.domain == builtin_provider_domain for provider in providers)
):
for domain in legacy_providers:
ir.async_create_issue(
hass,
DOMAIN,
f"legacy_webrtc_provider_{domain}",
is_fixable=False,
is_persistent=False,
issue_domain=domain,
learn_more_url="https://www.home-assistant.io/integrations/go2rtc/",
severity=ir.IssueSeverity.WARNING,
translation_key="legacy_webrtc_provider",
translation_placeholders={
"legacy_integration": domain,
"builtin_integration": builtin_provider_domain,
},
)
+2 -2
View File
@@ -10,12 +10,12 @@
"known_hosts": "Add known host"
},
"data_description": {
"known_hosts": "Hostnames or IP addresses of cast devices, use if mDNS discovery is not working"
"known_hosts": "Hostnames or IP-addresses of cast devices, use if mDNS discovery is not working"
}
}
},
"error": {
"invalid_known_hosts": "Known hosts must be a comma-separated list of hosts."
"invalid_known_hosts": "Known hosts must be a comma separated list of hosts."
}
},
"options": {
@@ -61,6 +61,7 @@ from .const import (
CONF_RELAYER_SERVER,
CONF_REMOTESTATE_SERVER,
CONF_SERVICEHANDLERS_SERVER,
CONF_THINGTALK_SERVER,
CONF_USER_POOL_ID,
DATA_CLOUD,
DATA_CLOUD_LOG_HANDLER,
@@ -133,6 +134,7 @@ CONFIG_SCHEMA = vol.Schema(
vol.Optional(CONF_CLOUDHOOK_SERVER): str,
vol.Optional(CONF_RELAYER_SERVER): str,
vol.Optional(CONF_REMOTESTATE_SERVER): str,
vol.Optional(CONF_THINGTALK_SERVER): str,
vol.Optional(CONF_SERVICEHANDLERS_SERVER): str,
}
)
+1 -10
View File
@@ -26,11 +26,7 @@ from homeassistant.core import Context, HassJob, HomeAssistant, callback
from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.issue_registry import (
IssueSeverity,
async_create_issue,
async_delete_issue,
)
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
from homeassistant.util.aiohttp import MockRequest, serialize_response
from . import alexa_config, google_config
@@ -40,7 +36,6 @@ from .prefs import CloudPreferences
_LOGGER = logging.getLogger(__name__)
VALID_REPAIR_TRANSLATION_KEYS = {
"no_subscription",
"warn_bad_custom_domain_configuration",
"reset_bad_custom_domain_configuration",
}
@@ -414,7 +409,3 @@ class CloudClient(Interface):
severity=IssueSeverity(severity),
is_fixable=False,
)
async def async_delete_repair_issue(self, identifier: str) -> None:
"""Delete a repair issue."""
async_delete_issue(hass=self._hass, domain=DOMAIN, issue_id=identifier)
+1
View File
@@ -81,6 +81,7 @@ CONF_ACME_SERVER = "acme_server"
CONF_CLOUDHOOK_SERVER = "cloudhook_server"
CONF_RELAYER_SERVER = "relayer_server"
CONF_REMOTESTATE_SERVER = "remotestate_server"
CONF_THINGTALK_SERVER = "thingtalk_server"
CONF_SERVICEHANDLERS_SERVER = "servicehandlers_server"
MODE_DEV = "development"
+21 -1
View File
@@ -16,7 +16,7 @@ from typing import Any, Concatenate, cast
import aiohttp
from aiohttp import web
import attr
from hass_nabucasa import AlreadyConnectedError, Cloud, auth
from hass_nabucasa import AlreadyConnectedError, Cloud, auth, thingtalk
from hass_nabucasa.const import STATE_DISCONNECTED
from hass_nabucasa.voice_data import TTS_VOICES
import voluptuous as vol
@@ -104,6 +104,7 @@ def async_setup(hass: HomeAssistant) -> None:
websocket_api.async_register_command(hass, alexa_list)
websocket_api.async_register_command(hass, alexa_sync)
websocket_api.async_register_command(hass, thingtalk_convert)
websocket_api.async_register_command(hass, tts_info)
hass.http.register_view(GoogleActionsSyncView)
@@ -997,6 +998,25 @@ async def alexa_sync(
)
@websocket_api.websocket_command({"type": "cloud/thingtalk/convert", "query": str})
@websocket_api.async_response
async def thingtalk_convert(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Convert a query."""
cloud = hass.data[DATA_CLOUD]
async with asyncio.timeout(10):
try:
connection.send_result(
msg["id"], await thingtalk.async_convert(cloud, msg["query"])
)
except thingtalk.ThingTalkConversionError as err:
connection.send_error(msg["id"], websocket_api.ERR_UNKNOWN_ERROR, str(err))
@websocket_api.websocket_command({"type": "cloud/tts/info"})
def tts_info(
hass: HomeAssistant,
+1 -1
View File
@@ -13,6 +13,6 @@
"integration_type": "system",
"iot_class": "cloud_push",
"loggers": ["acme", "hass_nabucasa", "snitun"],
"requirements": ["hass-nabucasa==0.100.0"],
"requirements": ["hass-nabucasa==0.96.0"],
"single_config_entry": true
}
@@ -62,10 +62,6 @@
}
}
},
"no_subscription": {
"title": "No subscription detected",
"description": "You do not have a Home Assistant Cloud subscription. Subscribe at {account_url}."
},
"warn_bad_custom_domain_configuration": {
"title": "Detected wrong custom domain configuration",
"description": "The DNS configuration for your custom domain ({custom_domains}) is not correct. Please check the DNS configuration of your domain and make sure it points to the correct CNAME."
@@ -77,5 +77,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ComelitConfigEntry) ->
coordinator = entry.runtime_data
if unload_ok := await hass.config_entries.async_unload_platforms(entry, platforms):
await coordinator.api.logout()
await coordinator.api.close()
return unload_ok
@@ -73,6 +73,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
) from err
finally:
await api.logout()
await api.close()
return {"title": data[CONF_HOST]}
@@ -65,8 +65,8 @@ rules:
status: todo
comment: missing implementation
entity-category:
status: exempt
comment: no config or diagnostic entities
status: todo
comment: PR in progress
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
@@ -76,7 +76,7 @@
"cannot_authenticate": {
"message": "Error authenticating"
},
"update_failed": {
"updated_failed": {
"message": "Failed to update data: {error}"
}
}
@@ -165,7 +165,9 @@ class ConfigManagerFlowIndexView(
"""Not implemented."""
raise aiohttp.web_exceptions.HTTPMethodNotAllowed("GET", ["POST"])
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission="add")
@require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
)
@RequestDataValidator(
vol.Schema(
{
@@ -216,12 +218,16 @@ class ConfigManagerFlowResourceView(
url = "/api/config/config_entries/flow/{flow_id}"
name = "api:config:config_entries:flow:resource"
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission="add")
@require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
)
async def get(self, request: web.Request, /, flow_id: str) -> web.Response:
"""Get the current state of a data_entry_flow."""
return await super().get(request, flow_id)
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission="add")
@require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission="add")
)
async def post(self, request: web.Request, flow_id: str) -> web.Response:
"""Handle a POST request."""
return await super().post(request, flow_id)
@@ -256,7 +262,9 @@ class OptionManagerFlowIndexView(
url = "/api/config/config_entries/options/flow"
name = "api:config:config_entries:option:flow"
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
@require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
)
async def post(self, request: web.Request) -> web.Response:
"""Handle a POST request.
@@ -273,12 +281,16 @@ class OptionManagerFlowResourceView(
url = "/api/config/config_entries/options/flow/{flow_id}"
name = "api:config:config_entries:options:flow:resource"
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
@require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
)
async def get(self, request: web.Request, /, flow_id: str) -> web.Response:
"""Get the current state of a data_entry_flow."""
return await super().get(request, flow_id)
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
@require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
)
async def post(self, request: web.Request, flow_id: str) -> web.Response:
"""Handle a POST request."""
return await super().post(request, flow_id)
@@ -292,7 +304,9 @@ class SubentryManagerFlowIndexView(
url = "/api/config/config_entries/subentries/flow"
name = "api:config:config_entries:subentries:flow"
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
@require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
)
@RequestDataValidator(
vol.Schema(
{
@@ -327,12 +341,16 @@ class SubentryManagerFlowResourceView(
url = "/api/config/config_entries/subentries/flow/{flow_id}"
name = "api:config:config_entries:subentries:flow:resource"
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
@require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
)
async def get(self, request: web.Request, /, flow_id: str) -> web.Response:
"""Get the current state of a data_entry_flow."""
return await super().get(request, flow_id)
@require_admin(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
@require_admin(
error=Unauthorized(perm_category=CAT_CONFIG_ENTRIES, permission=POLICY_EDIT)
)
async def post(self, request: web.Request, flow_id: str) -> web.Response:
"""Handle a POST request."""
return await super().post(request, flow_id)
@@ -9,13 +9,12 @@ import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components import websocket_api
from homeassistant.components.websocket_api import ERR_NOT_FOUND, require_admin
from homeassistant.core import HomeAssistant, callback, split_entity_id
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
)
from homeassistant.helpers.entity_component import async_get_entity_suggested_object_id
from homeassistant.helpers.json import json_dumps
@@ -23,7 +22,6 @@ from homeassistant.helpers.json import json_dumps
def async_setup(hass: HomeAssistant) -> bool:
"""Enable the Entity Registry views."""
websocket_api.async_register_command(hass, websocket_get_automatic_entity_ids)
websocket_api.async_register_command(hass, websocket_get_entities)
websocket_api.async_register_command(hass, websocket_get_entity)
websocket_api.async_register_command(hass, websocket_list_entities_for_display)
@@ -318,43 +316,3 @@ def websocket_remove_entity(
registry.async_remove(msg["entity_id"])
connection.send_message(websocket_api.result_message(msg["id"]))
@websocket_api.websocket_command(
{
vol.Required("type"): "config/entity_registry/get_automatic_entity_ids",
vol.Required("entity_ids"): cv.entity_ids,
}
)
@callback
def websocket_get_automatic_entity_ids(
hass: HomeAssistant,
connection: websocket_api.ActiveConnection,
msg: dict[str, Any],
) -> None:
"""Return the automatic entity IDs for the given entity IDs.
This is used to help user reset entity IDs which have been customized by the user.
"""
registry = er.async_get(hass)
entity_ids = msg["entity_ids"]
automatic_entity_ids: dict[str, str | None] = {}
for entity_id in entity_ids:
if not (entry := registry.entities.get(entity_id)):
automatic_entity_ids[entity_id] = None
continue
if (
suggested := async_get_entity_suggested_object_id(hass, entity_id)
) == split_entity_id(entry.entity_id)[1]:
# No need to generate a new entity ID
automatic_entity_ids[entity_id] = None
continue
automatic_entity_ids[entity_id] = registry.async_generate_entity_id(
entry.domain,
suggested or f"{entry.platform}_{entry.unique_id}",
)
connection.send_message(
websocket_api.result_message(msg["id"], automatic_entity_ids)
)
@@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
from . import DOMAIN as DANFOSS_AIR_DOMAIN
def setup_platform(
@@ -22,7 +22,7 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the available Danfoss Air sensors etc."""
data = hass.data[DOMAIN]
data = hass.data[DANFOSS_AIR_DOMAIN]
sensors = [
[
@@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
from . import DOMAIN as DANFOSS_AIR_DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -28,7 +28,7 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the available Danfoss Air sensors etc."""
data = hass.data[DOMAIN]
data = hass.data[DANFOSS_AIR_DOMAIN]
sensors = [
[
@@ -12,7 +12,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
from . import DOMAIN as DANFOSS_AIR_DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -24,7 +24,7 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Danfoss Air HRV switch platform."""
data = hass.data[DOMAIN]
data = hass.data[DANFOSS_AIR_DOMAIN]
switches = [
[
+5 -5
View File
@@ -13,7 +13,7 @@ from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE, DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from .const import DOMAIN
from .const import DOMAIN as DECONZ_DOMAIN
from .hub import DeconzHub
from .util import serial_from_unique_id
@@ -59,12 +59,12 @@ class DeconzBase[_DeviceT: _DeviceType]:
return DeviceInfo(
connections={(CONNECTION_ZIGBEE, self.serial)},
identifiers={(DOMAIN, self.serial)},
identifiers={(DECONZ_DOMAIN, self.serial)},
manufacturer=self._device.manufacturer,
model=self._device.model_id,
name=self._device.name,
sw_version=self._device.software_version,
via_device=(DOMAIN, self.hub.api.config.bridge_id),
via_device=(DECONZ_DOMAIN, self.hub.api.config.bridge_id),
)
@@ -176,9 +176,9 @@ class DeconzSceneMixin(DeconzDevice[PydeconzScene]):
def device_info(self) -> DeviceInfo:
"""Return a device description for device registry."""
return DeviceInfo(
identifiers={(DOMAIN, self._group_identifier)},
identifiers={(DECONZ_DOMAIN, self._group_identifier)},
manufacturer="Dresden Elektronik",
model="deCONZ group",
name=self.group.name,
via_device=(DOMAIN, self.hub.api.config.bridge_id),
via_device=(DECONZ_DOMAIN, self.hub.api.config.bridge_id),
)
+3 -3
View File
@@ -38,7 +38,7 @@ from homeassistant.util.color import (
)
from . import DeconzConfigEntry
from .const import DOMAIN, POWER_PLUGS
from .const import DOMAIN as DECONZ_DOMAIN, POWER_PLUGS
from .entity import DeconzDevice
from .hub import DeconzHub
@@ -395,11 +395,11 @@ class DeconzGroup(DeconzBaseLight[Group]):
def device_info(self) -> DeviceInfo:
"""Return a device description for device registry."""
return DeviceInfo(
identifiers={(DOMAIN, self.unique_id)},
identifiers={(DECONZ_DOMAIN, self.unique_id)},
manufacturer="Dresden Elektronik",
model="deCONZ group",
name=self._device.name,
via_device=(DOMAIN, self.hub.api.config.bridge_id),
via_device=(DECONZ_DOMAIN, self.hub.api.config.bridge_id),
)
@property
@@ -6,7 +6,7 @@
"documentation": "https://www.home-assistant.io/integrations/denonavr",
"iot_class": "local_push",
"loggers": ["denonavr"],
"requirements": ["denonavr==1.1.0"],
"requirements": ["denonavr==1.0.1"],
"ssdp": [
{
"manufacturer": "Denon",
@@ -15,7 +15,7 @@
},
"data_description": {
"round": "Controls the number of decimal digits in the output.",
"time_window": "If set, the sensor's value is a time-weighted moving average of derivatives within this window.",
"time_window": "If set, the sensor's value is a time weighted moving average of derivatives within this window.",
"unit_prefix": "The output will be scaled according to the selected metric prefix and time unit of the derivative."
}
}
+1 -1
View File
@@ -15,7 +15,7 @@
"quality_scale": "internal",
"requirements": [
"aiodhcpwatcher==1.1.1",
"aiodiscover==2.7.0",
"aiodiscover==2.6.1",
"cached-ipaddress==0.10.0"
]
}
@@ -3,10 +3,10 @@
"step": {
"init": {
"data": {
"events": "Comma-separated list of events."
"events": "Comma separated list of events."
},
"data_description": {
"events": "Add a comma-separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event.\n\nExample: somebody_pressed_the_button, motion"
"events": "Add a comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event.\n\nExample: somebody_pressed_the_button, motion"
}
}
}
+2 -2
View File
@@ -8,7 +8,7 @@ from homeassistant.components.notify import ATTR_TARGET, BaseNotificationService
from homeassistant.core import HomeAssistant
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
from . import DOMAIN as DOVADO_DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -19,7 +19,7 @@ def get_service(
discovery_info: DiscoveryInfoType | None = None,
) -> DovadoSMSNotificationService:
"""Get the Dovado Router SMS notification service."""
return DovadoSMSNotificationService(hass.data[DOMAIN].client)
return DovadoSMSNotificationService(hass.data[DOVADO_DOMAIN].client)
class DovadoSMSNotificationService(BaseNotificationService):
+2 -2
View File
@@ -20,7 +20,7 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from . import DOMAIN
from . import DOMAIN as DOVADO_DOMAIN
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
@@ -90,7 +90,7 @@ def setup_platform(
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Dovado sensor platform."""
dovado = hass.data[DOMAIN]
dovado = hass.data[DOVADO_DOMAIN]
sensors = config[CONF_SENSORS]
entities = [
@@ -3,12 +3,12 @@
"step": {
"user": {
"data": {
"phone_number": "Phone number"
"phone_number": "Phone Number"
}
},
"one_time_password": {
"data": {
"one_time_password": "One-time password"
"one_time_password": "One Time Password"
}
}
},
@@ -7,12 +7,12 @@
"step": {
"user": {
"data": {
"advertise_ip": "Advertise IP address",
"advertise_port": "Advertise port",
"host_ip": "Host IP address",
"listen_port": "Listen port",
"advertise_ip": "Advertise IP Address",
"advertise_port": "Advertise Port",
"host_ip": "Host IP Address",
"listen_port": "Listen Port",
"name": "[%key:common::config_flow::data::name%]",
"upnp_bind_multicast": "Bind multicast"
"upnp_bind_multicast": "Bind multicast (True/False)"
},
"title": "Define server configuration"
}
@@ -52,7 +52,6 @@ VALID_ENERGY_UNITS_GAS = {
UnitOfVolume.CENTUM_CUBIC_FEET,
UnitOfVolume.CUBIC_FEET,
UnitOfVolume.CUBIC_METERS,
UnitOfVolume.LITERS,
*VALID_ENERGY_UNITS,
}
VALID_VOLUME_UNITS_WATER: set[str] = {
@@ -50,7 +50,6 @@ GAS_USAGE_UNITS: dict[str, tuple[UnitOfEnergy | UnitOfVolume, ...]] = {
UnitOfVolume.CENTUM_CUBIC_FEET,
UnitOfVolume.CUBIC_FEET,
UnitOfVolume.CUBIC_METERS,
UnitOfVolume.LITERS,
),
}
GAS_PRICE_UNITS = tuple(
@@ -64,7 +64,7 @@ async def _get_fixture_collection(envoy: Envoy, serial: str) -> dict[str, Any]:
"/ivp/ensemble/generator",
"/ivp/meters",
"/ivp/meters/readings",
"/home",
"/home,",
]
for end_point in end_points:
@@ -7,7 +7,7 @@
"iot_class": "local_polling",
"loggers": ["pyenphase"],
"quality_scale": "platinum",
"requirements": ["pyenphase==1.26.1"],
"requirements": ["pyenphase==1.26.0"],
"zeroconf": [
{
"type": "_enphase-envoy._tcp.local."
@@ -134,22 +134,6 @@ def esphome_state_property[_R, _EntityT: EsphomeEntity[Any, Any]](
return _wrapper
def async_esphome_state_property[_R, _EntityT: EsphomeEntity[Any, Any]](
func: Callable[[_EntityT], Awaitable[_R | None]],
) -> Callable[[_EntityT], Coroutine[Any, Any, _R | None]]:
"""Wrap a state property of an esphome entity.
This checks if the state object in the entity is set
and returns None if it is not set.
"""
@functools.wraps(func)
async def _wrapper(self: _EntityT) -> _R | None:
return await func(self) if self._has_state else None
return _wrapper
def esphome_float_state_property[_EntityT: EsphomeEntity[Any, Any]](
func: Callable[[_EntityT], float | None],
) -> Callable[[_EntityT], float | None]:
+56 -9
View File
@@ -8,7 +8,6 @@ from collections.abc import Callable, Iterable
from dataclasses import dataclass, field
from functools import partial
import logging
from operator import delitem
from typing import TYPE_CHECKING, Any, Final, TypedDict, cast
from aioesphomeapi import (
@@ -184,7 +183,18 @@ class RuntimeEntryData:
"""Register to receive callbacks when static info changes for an EntityInfo type."""
callbacks = self.entity_info_callbacks.setdefault(entity_info_type, [])
callbacks.append(callback_)
return partial(callbacks.remove, callback_)
return partial(
self._async_unsubscribe_register_static_info, callbacks, callback_
)
@callback
def _async_unsubscribe_register_static_info(
self,
callbacks: list[Callable[[list[EntityInfo]], None]],
callback_: Callable[[list[EntityInfo]], None],
) -> None:
"""Unsubscribe to when static info is registered."""
callbacks.remove(callback_)
@callback
def async_register_key_static_info_updated_callback(
@@ -196,7 +206,18 @@ class RuntimeEntryData:
callback_key = (type(static_info), static_info.key)
callbacks = self.entity_info_key_updated_callbacks.setdefault(callback_key, [])
callbacks.append(callback_)
return partial(callbacks.remove, callback_)
return partial(
self._async_unsubscribe_static_key_info_updated, callbacks, callback_
)
@callback
def _async_unsubscribe_static_key_info_updated(
self,
callbacks: list[Callable[[EntityInfo], None]],
callback_: Callable[[EntityInfo], None],
) -> None:
"""Unsubscribe to when static info is updated ."""
callbacks.remove(callback_)
@callback
def async_set_assist_pipeline_state(self, state: bool) -> None:
@@ -211,7 +232,14 @@ class RuntimeEntryData:
) -> CALLBACK_TYPE:
"""Subscribe to assist pipeline updates."""
self.assist_pipeline_update_callbacks.append(update_callback)
return partial(self.assist_pipeline_update_callbacks.remove, update_callback)
return partial(self._async_unsubscribe_assist_pipeline_update, update_callback)
@callback
def _async_unsubscribe_assist_pipeline_update(
self, update_callback: CALLBACK_TYPE
) -> None:
"""Unsubscribe to assist pipeline updates."""
self.assist_pipeline_update_callbacks.remove(update_callback)
@callback
def async_remove_entities(
@@ -309,7 +337,12 @@ class RuntimeEntryData:
def async_subscribe_device_updated(self, callback_: CALLBACK_TYPE) -> CALLBACK_TYPE:
"""Subscribe to state updates."""
self.device_update_subscriptions.add(callback_)
return partial(self.device_update_subscriptions.remove, callback_)
return partial(self._async_unsubscribe_device_update, callback_)
@callback
def _async_unsubscribe_device_update(self, callback_: CALLBACK_TYPE) -> None:
"""Unsubscribe to device updates."""
self.device_update_subscriptions.remove(callback_)
@callback
def async_subscribe_static_info_updated(
@@ -317,7 +350,14 @@ class RuntimeEntryData:
) -> CALLBACK_TYPE:
"""Subscribe to static info updates."""
self.static_info_update_subscriptions.add(callback_)
return partial(self.static_info_update_subscriptions.remove, callback_)
return partial(self._async_unsubscribe_static_info_updated, callback_)
@callback
def _async_unsubscribe_static_info_updated(
self, callback_: Callable[[list[EntityInfo]], None]
) -> None:
"""Unsubscribe to static info updates."""
self.static_info_update_subscriptions.remove(callback_)
@callback
def async_subscribe_state_update(
@@ -329,7 +369,14 @@ class RuntimeEntryData:
"""Subscribe to state updates."""
subscription_key = (state_type, state_key)
self.state_subscriptions[subscription_key] = entity_callback
return partial(delitem, self.state_subscriptions, subscription_key)
return partial(self._async_unsubscribe_state_update, subscription_key)
@callback
def _async_unsubscribe_state_update(
self, subscription_key: tuple[type[EntityState], int]
) -> None:
"""Unsubscribe to state updates."""
self.state_subscriptions.pop(subscription_key)
@callback
def async_update_state(self, state: EntityState) -> None:
@@ -476,7 +523,7 @@ class RuntimeEntryData:
) -> CALLBACK_TYPE:
"""Register to receive callbacks when the Assist satellite's configuration is updated."""
self.assist_satellite_config_update_callbacks.append(callback_)
return partial(self.assist_satellite_config_update_callbacks.remove, callback_)
return lambda: self.assist_satellite_config_update_callbacks.remove(callback_)
@callback
def async_assist_satellite_config_updated(
@@ -493,7 +540,7 @@ class RuntimeEntryData:
) -> CALLBACK_TYPE:
"""Register to receive callbacks when the Assist satellite's wake word is set."""
self.assist_satellite_set_wake_word_callbacks.append(callback_)
return partial(self.assist_satellite_set_wake_word_callbacks.remove, callback_)
return lambda: self.assist_satellite_set_wake_word_callbacks.remove(callback_)
@callback
def async_assist_satellite_set_wake_word(self, wake_word_id: str) -> None:
@@ -17,7 +17,7 @@
"mqtt": ["esphome/discover/#"],
"quality_scale": "platinum",
"requirements": [
"aioesphomeapi==30.2.0",
"aioesphomeapi==30.1.0",
"esphome-dashboard-api==1.3.0",
"bleak-esphome==2.15.1"
],
@@ -195,10 +195,7 @@
"message": "Error compiling {configuration}; Try again in ESPHome dashboard for more information."
},
"error_uploading": {
"message": "Error during OTA (Over-The-Air) of {configuration}; Try again in ESPHome dashboard for more information."
},
"ota_in_progress": {
"message": "An OTA (Over-The-Air) update is already in progress for {configuration}."
"message": "Error during OTA of {configuration}; Try again in ESPHome dashboard for more information."
}
}
}
+28 -75
View File
@@ -31,7 +31,6 @@ from .coordinator import ESPHomeDashboardCoordinator
from .dashboard import async_get_dashboard
from .entity import (
EsphomeEntity,
async_esphome_state_property,
convert_api_error_ha_error,
esphome_state_property,
platform_async_setup_entry,
@@ -126,17 +125,21 @@ class ESPHomeDashboardUpdateEntity(
(dr.CONNECTION_NETWORK_MAC, entry_data.device_info.mac_address)
}
)
self._install_lock = asyncio.Lock()
self._available_future: asyncio.Future[None] | None = None
self._update_attrs()
@callback
def _update_attrs(self) -> None:
"""Update the supported features."""
# If the device has deep sleep, we can't assume we can install updates
# as the ESP will not be connectable (by design).
coordinator = self.coordinator
device_info = self._device_info
# Install support can change at run time
if coordinator.last_update_success and coordinator.supports_update:
if (
coordinator.last_update_success
and coordinator.supports_update
and not device_info.has_deep_sleep
):
self._attr_supported_features = UpdateEntityFeature.INSTALL
else:
self._attr_supported_features = NO_FEATURES
@@ -175,13 +178,6 @@ class ESPHomeDashboardUpdateEntity(
self, static_info: list[EntityInfo] | None = None
) -> None:
"""Handle updated data from the device."""
if (
self._entry_data.available
and self._available_future
and not self._available_future.done()
):
self._available_future.set_result(None)
self._available_future = None
self._update_attrs()
self.async_write_ha_state()
@@ -196,46 +192,17 @@ class ESPHomeDashboardUpdateEntity(
entry_data.async_subscribe_device_updated(self._handle_device_update)
)
async def async_will_remove_from_hass(self) -> None:
"""Handle entity about to be removed from Home Assistant."""
if self._available_future and not self._available_future.done():
self._available_future.cancel()
self._available_future = None
async def _async_wait_available(self) -> None:
"""Wait until the device is available."""
# If the device has deep sleep, we need to wait for it to wake up
# and connect to the network to be able to install the update.
if self._entry_data.available:
return
self._available_future = self.hass.loop.create_future()
try:
await self._available_future
finally:
self._available_future = None
async def async_install(
self, version: str | None, backup: bool, **kwargs: Any
) -> None:
"""Install an update."""
if self._install_lock.locked():
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="ota_in_progress",
translation_placeholders={
"configuration": self._device_info.name,
},
)
# Ensure only one OTA per device at a time
async with self._install_lock:
# Ensure only one compile at a time for ALL devices
async with self.hass.data.setdefault(KEY_UPDATE_LOCK, asyncio.Lock()):
coordinator = self.coordinator
api = coordinator.api
device = coordinator.data.get(self._device_info.name)
assert device is not None
configuration = device["configuration"]
async with self.hass.data.setdefault(KEY_UPDATE_LOCK, asyncio.Lock()):
coordinator = self.coordinator
api = coordinator.api
device = coordinator.data.get(self._device_info.name)
assert device is not None
configuration = device["configuration"]
try:
if not await api.compile(configuration):
raise HomeAssistantError(
translation_domain=DOMAIN,
@@ -244,25 +211,14 @@ class ESPHomeDashboardUpdateEntity(
"configuration": configuration,
},
)
# If the device uses deep sleep, there's a small chance it goes
# to sleep right after the dashboard connects but before the OTA
# starts. In that case, the update won't go through, so we try
# again to catch it on its next wakeup.
attempts = 2 if self._device_info.has_deep_sleep else 1
try:
for attempt in range(1, attempts + 1):
await self._async_wait_available()
if await api.upload(configuration, "OTA"):
break
if attempt == attempts:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="error_uploading",
translation_placeholders={
"configuration": configuration,
},
)
if not await api.upload(configuration, "OTA"):
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="error_uploading",
translation_placeholders={
"configuration": configuration,
},
)
finally:
await self.coordinator.async_request_refresh()
@@ -271,9 +227,7 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity):
"""A update implementation for esphome."""
_attr_supported_features = (
UpdateEntityFeature.INSTALL
| UpdateEntityFeature.PROGRESS
| UpdateEntityFeature.RELEASE_NOTES
UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
)
@callback
@@ -303,12 +257,11 @@ class ESPHomeUpdateEntity(EsphomeEntity[UpdateInfo, UpdateState], UpdateEntity):
"""Return the latest version."""
return self._state.latest_version
@async_esphome_state_property
async def async_release_notes(self) -> str | None:
"""Return the release notes."""
if self._state.release_summary:
return self._state.release_summary
return None
@property
@esphome_state_property
def release_summary(self) -> str:
"""Return the release summary."""
return self._state.release_summary
@property
@esphome_state_property
+2 -2
View File
@@ -2,8 +2,8 @@
import logging
from pyezvizapi.client import EzvizClient
from pyezvizapi.exceptions import (
from pyezviz.client import EzvizClient
from pyezviz.exceptions import (
EzvizAuthTokenExpired,
EzvizAuthVerificationCode,
HTTPError,
@@ -6,8 +6,8 @@ from dataclasses import dataclass
from datetime import timedelta
import logging
from pyezvizapi import PyEzvizError
from pyezvizapi.constants import DefenseModeType
from pyezviz import PyEzvizError
from pyezviz.constants import DefenseModeType
from homeassistant.components.alarm_control_panel import (
AlarmControlPanelEntity,
+3 -3
View File
@@ -6,9 +6,9 @@ from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from pyezvizapi import EzvizClient
from pyezvizapi.constants import SupportExt
from pyezvizapi.exceptions import HTTPError, PyEzvizError
from pyezviz import EzvizClient
from pyezviz.constants import SupportExt
from pyezviz.exceptions import HTTPError, PyEzvizError
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.core import HomeAssistant
+1 -1
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
import logging
from pyezvizapi.exceptions import HTTPError, InvalidHost, PyEzvizError
from pyezviz.exceptions import HTTPError, InvalidHost, PyEzvizError
from homeassistant.components import ffmpeg
from homeassistant.components.camera import Camera, CameraEntityFeature
@@ -6,15 +6,15 @@ from collections.abc import Mapping
import logging
from typing import TYPE_CHECKING, Any
from pyezvizapi.client import EzvizClient
from pyezvizapi.exceptions import (
from pyezviz.client import EzvizClient
from pyezviz.exceptions import (
AuthTestResultFailed,
EzvizAuthVerificationCode,
InvalidHost,
InvalidURL,
PyEzvizError,
)
from pyezvizapi.test_cam_rtsp import TestRTSPAuth
from pyezviz.test_cam_rtsp import TestRTSPAuth
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
@@ -4,8 +4,8 @@ import asyncio
from datetime import timedelta
import logging
from pyezvizapi.client import EzvizClient
from pyezvizapi.exceptions import (
from pyezviz.client import EzvizClient
from pyezviz.exceptions import (
EzvizAuthTokenExpired,
EzvizAuthVerificationCode,
HTTPError,
+2 -2
View File
@@ -5,8 +5,8 @@ from __future__ import annotations
import logging
from propcache.api import cached_property
from pyezvizapi.exceptions import PyEzvizError
from pyezvizapi.utils import decrypt_image
from pyezviz.exceptions import PyEzvizError
from pyezviz.utils import decrypt_image
from homeassistant.components.image import Image, ImageEntity, ImageEntityDescription
from homeassistant.config_entries import SOURCE_IGNORE
+2 -2
View File
@@ -4,8 +4,8 @@ from __future__ import annotations
from typing import Any
from pyezvizapi.constants import DeviceCatagories, DeviceSwitchType, SupportExt
from pyezvizapi.exceptions import HTTPError, PyEzvizError
from pyezviz.constants import DeviceCatagories, DeviceSwitchType, SupportExt
from pyezviz.exceptions import HTTPError, PyEzvizError
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.core import HomeAssistant, callback
+3 -3
View File
@@ -1,11 +1,11 @@
{
"domain": "ezviz",
"name": "EZVIZ",
"codeowners": ["@RenierM26"],
"codeowners": ["@RenierM26", "@baqs"],
"config_flow": true,
"dependencies": ["ffmpeg"],
"documentation": "https://www.home-assistant.io/integrations/ezviz",
"iot_class": "cloud_polling",
"loggers": ["paho_mqtt", "pyezvizapi"],
"requirements": ["pyezvizapi==1.0.0.7"]
"loggers": ["paho_mqtt", "pyezviz"],
"requirements": ["pyezviz==0.2.1.2"]
}
+2 -2
View File
@@ -6,8 +6,8 @@ from dataclasses import dataclass
from datetime import timedelta
import logging
from pyezvizapi.constants import SupportExt
from pyezvizapi.exceptions import (
from pyezviz.constants import SupportExt
from pyezviz.exceptions import (
EzvizAuthTokenExpired,
EzvizAuthVerificationCode,
HTTPError,
+2 -2
View File
@@ -4,8 +4,8 @@ from __future__ import annotations
from dataclasses import dataclass
from pyezvizapi.constants import DeviceSwitchType, SoundMode
from pyezvizapi.exceptions import HTTPError, PyEzvizError
from pyezviz.constants import DeviceSwitchType, SoundMode
from pyezviz.exceptions import HTTPError, PyEzvizError
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.const import EntityCategory
+1 -1
View File
@@ -6,7 +6,7 @@ from collections.abc import Callable
from datetime import datetime, timedelta
from typing import Any
from pyezvizapi import HTTPError, PyEzvizError, SupportExt
from pyezviz import HTTPError, PyEzvizError, SupportExt
from homeassistant.components.siren import (
SirenEntity,

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