mirror of
https://github.com/home-assistant/core.git
synced 2025-11-15 05:50:13 +00:00
Merge branch 'home-assistant:dev' into flussButton
This commit is contained in:
6
.github/workflows/builder.yml
vendored
6
.github/workflows/builder.yml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
if: needs.init.outputs.channel == 'dev'
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
@@ -453,7 +453,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
|
||||
32
.github/workflows/ci.yaml
vendored
32
.github/workflows/ci.yaml
vendored
@@ -229,7 +229,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -274,7 +274,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -314,7 +314,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -353,7 +353,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
id: python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
@@ -448,7 +448,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -532,7 +532,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -564,7 +564,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -595,7 +595,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -637,7 +637,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -682,7 +682,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -726,7 +726,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -800,7 +800,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
@@ -863,7 +863,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -981,7 +981,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -1106,7 +1106,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
@@ -1251,7 +1251,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: true
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3.25.11
|
||||
uses: github/codeql-action/init@v3.25.12
|
||||
with:
|
||||
languages: python
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3.25.11
|
||||
uses: github/codeql-action/analyze@v3.25.12
|
||||
with:
|
||||
category: "/language:python"
|
||||
|
||||
2
.github/workflows/translations.yml
vendored
2
.github/workflows/translations.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
uses: actions/checkout@v4.1.7
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
|
||||
|
||||
2
.github/workflows/wheels.yml
vendored
2
.github/workflows/wheels.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@v5.1.0
|
||||
uses: actions/setup-python@v5.1.1
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
check-latest: true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.5.1
|
||||
rev: v0.5.2
|
||||
hooks:
|
||||
- id: ruff
|
||||
args:
|
||||
|
||||
@@ -97,6 +97,7 @@ homeassistant.components.assist_pipeline.*
|
||||
homeassistant.components.asterisk_cdr.*
|
||||
homeassistant.components.asterisk_mbox.*
|
||||
homeassistant.components.asuswrt.*
|
||||
homeassistant.components.autarco.*
|
||||
homeassistant.components.auth.*
|
||||
homeassistant.components.automation.*
|
||||
homeassistant.components.awair.*
|
||||
|
||||
@@ -155,6 +155,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/aurora_abb_powerone/ @davet2001
|
||||
/homeassistant/components/aussie_broadband/ @nickw444 @Bre77
|
||||
/tests/components/aussie_broadband/ @nickw444 @Bre77
|
||||
/homeassistant/components/autarco/ @klaasnicolaas
|
||||
/tests/components/autarco/ @klaasnicolaas
|
||||
/homeassistant/components/auth/ @home-assistant/core
|
||||
/tests/components/auth/ @home-assistant/core
|
||||
/homeassistant/components/automation/ @home-assistant/core
|
||||
@@ -706,6 +708,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/isal/ @bdraco
|
||||
/homeassistant/components/islamic_prayer_times/ @engrbm87 @cpfair
|
||||
/tests/components/islamic_prayer_times/ @engrbm87 @cpfair
|
||||
/homeassistant/components/israel_rail/ @shaiu
|
||||
/tests/components/israel_rail/ @shaiu
|
||||
/homeassistant/components/iss/ @DurgNomis-drol
|
||||
/tests/components/iss/ @DurgNomis-drol
|
||||
/homeassistant/components/ista_ecotrend/ @tr4nt0r
|
||||
@@ -1209,6 +1213,8 @@ build.json @home-assistant/supervisor
|
||||
/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
|
||||
/tests/components/russound_rio/ @noahhusby
|
||||
/homeassistant/components/ruuvi_gateway/ @akx
|
||||
/tests/components/ruuvi_gateway/ @akx
|
||||
/homeassistant/components/ruuvitag_ble/ @akx
|
||||
|
||||
@@ -82,33 +82,54 @@ async def async_setup_entry(
|
||||
"""Add Airzone binary sensors from a config_entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
binary_sensors: list[AirzoneBinarySensor] = [
|
||||
AirzoneSystemBinarySensor(
|
||||
coordinator,
|
||||
description,
|
||||
entry,
|
||||
system_id,
|
||||
system_data,
|
||||
)
|
||||
for system_id, system_data in coordinator.data[AZD_SYSTEMS].items()
|
||||
for description in SYSTEM_BINARY_SENSOR_TYPES
|
||||
if description.key in system_data
|
||||
]
|
||||
added_systems: set[str] = set()
|
||||
added_zones: set[str] = set()
|
||||
|
||||
binary_sensors.extend(
|
||||
AirzoneZoneBinarySensor(
|
||||
coordinator,
|
||||
description,
|
||||
entry,
|
||||
system_zone_id,
|
||||
zone_data,
|
||||
)
|
||||
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
|
||||
for description in ZONE_BINARY_SENSOR_TYPES
|
||||
if description.key in zone_data
|
||||
)
|
||||
def _async_entity_listener() -> None:
|
||||
"""Handle additions of binary sensors."""
|
||||
|
||||
async_add_entities(binary_sensors)
|
||||
entities: list[AirzoneBinarySensor] = []
|
||||
|
||||
systems_data = coordinator.data.get(AZD_SYSTEMS, {})
|
||||
received_systems = set(systems_data)
|
||||
new_systems = received_systems - added_systems
|
||||
if new_systems:
|
||||
entities.extend(
|
||||
AirzoneSystemBinarySensor(
|
||||
coordinator,
|
||||
description,
|
||||
entry,
|
||||
system_id,
|
||||
systems_data.get(system_id),
|
||||
)
|
||||
for system_id in new_systems
|
||||
for description in SYSTEM_BINARY_SENSOR_TYPES
|
||||
if description.key in systems_data.get(system_id)
|
||||
)
|
||||
added_systems.update(new_systems)
|
||||
|
||||
zones_data = coordinator.data.get(AZD_ZONES, {})
|
||||
received_zones = set(zones_data)
|
||||
new_zones = received_zones - added_zones
|
||||
if new_zones:
|
||||
entities.extend(
|
||||
AirzoneZoneBinarySensor(
|
||||
coordinator,
|
||||
description,
|
||||
entry,
|
||||
system_zone_id,
|
||||
zones_data.get(system_zone_id),
|
||||
)
|
||||
for system_zone_id in new_zones
|
||||
for description in ZONE_BINARY_SENSOR_TYPES
|
||||
if description.key in zones_data.get(system_zone_id)
|
||||
)
|
||||
added_zones.update(new_zones)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
|
||||
_async_entity_listener()
|
||||
|
||||
|
||||
class AirzoneBinarySensor(AirzoneEntity, BinarySensorEntity):
|
||||
|
||||
@@ -102,17 +102,31 @@ async def async_setup_entry(
|
||||
entry: AirzoneConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add Airzone sensors from a config_entry."""
|
||||
"""Add Airzone climate from a config_entry."""
|
||||
coordinator = entry.runtime_data
|
||||
async_add_entities(
|
||||
AirzoneClimate(
|
||||
coordinator,
|
||||
entry,
|
||||
system_zone_id,
|
||||
zone_data,
|
||||
)
|
||||
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
|
||||
)
|
||||
|
||||
added_zones: set[str] = set()
|
||||
|
||||
def _async_entity_listener() -> None:
|
||||
"""Handle additions of climate."""
|
||||
|
||||
zones_data = coordinator.data.get(AZD_ZONES, {})
|
||||
received_zones = set(zones_data)
|
||||
new_zones = received_zones - added_zones
|
||||
if new_zones:
|
||||
async_add_entities(
|
||||
AirzoneClimate(
|
||||
coordinator,
|
||||
entry,
|
||||
system_zone_id,
|
||||
zones_data.get(system_zone_id),
|
||||
)
|
||||
for system_zone_id in new_zones
|
||||
)
|
||||
added_zones.update(new_zones)
|
||||
|
||||
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
|
||||
_async_entity_listener()
|
||||
|
||||
|
||||
class AirzoneClimate(AirzoneZoneEntity, ClimateEntity):
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioairzone"],
|
||||
"requirements": ["aioairzone==0.7.7"]
|
||||
"requirements": ["aioairzone==0.8.0"]
|
||||
}
|
||||
|
||||
@@ -83,21 +83,34 @@ async def async_setup_entry(
|
||||
entry: AirzoneConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add Airzone sensors from a config_entry."""
|
||||
"""Add Airzone select from a config_entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
AirzoneZoneSelect(
|
||||
coordinator,
|
||||
description,
|
||||
entry,
|
||||
system_zone_id,
|
||||
zone_data,
|
||||
)
|
||||
for description in ZONE_SELECT_TYPES
|
||||
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
|
||||
if description.key in zone_data
|
||||
)
|
||||
added_zones: set[str] = set()
|
||||
|
||||
def _async_entity_listener() -> None:
|
||||
"""Handle additions of select."""
|
||||
|
||||
zones_data = coordinator.data.get(AZD_ZONES, {})
|
||||
received_zones = set(zones_data)
|
||||
new_zones = received_zones - added_zones
|
||||
if new_zones:
|
||||
async_add_entities(
|
||||
AirzoneZoneSelect(
|
||||
coordinator,
|
||||
description,
|
||||
entry,
|
||||
system_zone_id,
|
||||
zones_data.get(system_zone_id),
|
||||
)
|
||||
for system_zone_id in new_zones
|
||||
for description in ZONE_SELECT_TYPES
|
||||
if description.key in zones_data.get(system_zone_id)
|
||||
)
|
||||
added_zones.update(new_zones)
|
||||
|
||||
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
|
||||
_async_entity_listener()
|
||||
|
||||
|
||||
class AirzoneBaseSelect(AirzoneEntity, SelectEntity):
|
||||
|
||||
@@ -85,21 +85,37 @@ async def async_setup_entry(
|
||||
"""Add Airzone sensors from a config_entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
sensors: list[AirzoneSensor] = [
|
||||
AirzoneZoneSensor(
|
||||
coordinator,
|
||||
description,
|
||||
entry,
|
||||
system_zone_id,
|
||||
zone_data,
|
||||
)
|
||||
for system_zone_id, zone_data in coordinator.data[AZD_ZONES].items()
|
||||
for description in ZONE_SENSOR_TYPES
|
||||
if description.key in zone_data
|
||||
]
|
||||
added_zones: set[str] = set()
|
||||
|
||||
def _async_entity_listener() -> None:
|
||||
"""Handle additions of sensors."""
|
||||
|
||||
entities: list[AirzoneSensor] = []
|
||||
|
||||
zones_data = coordinator.data.get(AZD_ZONES, {})
|
||||
received_zones = set(zones_data)
|
||||
new_zones = received_zones - added_zones
|
||||
if new_zones:
|
||||
entities.extend(
|
||||
AirzoneZoneSensor(
|
||||
coordinator,
|
||||
description,
|
||||
entry,
|
||||
system_zone_id,
|
||||
zones_data.get(system_zone_id),
|
||||
)
|
||||
for system_zone_id in new_zones
|
||||
for description in ZONE_SENSOR_TYPES
|
||||
if description.key in zones_data.get(system_zone_id)
|
||||
)
|
||||
added_zones.update(new_zones)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
entities: list[AirzoneSensor] = []
|
||||
|
||||
if AZD_HOT_WATER in coordinator.data:
|
||||
sensors.extend(
|
||||
entities.extend(
|
||||
AirzoneHotWaterSensor(
|
||||
coordinator,
|
||||
description,
|
||||
@@ -110,7 +126,7 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
if AZD_WEBSERVER in coordinator.data:
|
||||
sensors.extend(
|
||||
entities.extend(
|
||||
AirzoneWebServerSensor(
|
||||
coordinator,
|
||||
description,
|
||||
@@ -120,7 +136,10 @@ async def async_setup_entry(
|
||||
if description.key in coordinator.data[AZD_WEBSERVER]
|
||||
)
|
||||
|
||||
async_add_entities(sensors)
|
||||
async_add_entities(entities)
|
||||
|
||||
entry.async_on_unload(coordinator.async_add_listener(_async_entity_listener))
|
||||
_async_entity_listener()
|
||||
|
||||
|
||||
class AirzoneSensor(AirzoneEntity, SensorEntity):
|
||||
|
||||
@@ -61,7 +61,7 @@ async def async_setup_entry(
|
||||
entry: AirzoneConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Add Airzone sensors from a config_entry."""
|
||||
"""Add Airzone Water Heater from a config_entry."""
|
||||
coordinator = entry.runtime_data
|
||||
if AZD_HOT_WATER in coordinator.data:
|
||||
async_add_entities([AirzoneWaterHeater(coordinator, entry)])
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.5.3"]
|
||||
"requirements": ["aioairzone-cloud==0.5.4"]
|
||||
}
|
||||
|
||||
@@ -1513,7 +1513,7 @@ async def async_api_adjust_range(
|
||||
if instance == f"{cover.DOMAIN}.{cover.ATTR_POSITION}":
|
||||
range_delta = int(range_delta * 20) if range_delta_default else int(range_delta)
|
||||
service = SERVICE_SET_COVER_POSITION
|
||||
if not (current := entity.attributes.get(cover.ATTR_POSITION)):
|
||||
if not (current := entity.attributes.get(cover.ATTR_CURRENT_POSITION)):
|
||||
msg = f"Unable to determine {entity.entity_id} current position"
|
||||
raise AlexaInvalidValueError(msg)
|
||||
position = response_value = min(100, max(0, range_delta + current))
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/amazon_polly",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["boto3", "botocore", "s3transfer"],
|
||||
"requirements": ["boto3==1.34.51"]
|
||||
"requirements": ["boto3==1.34.131"]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["python_homeassistant_analytics"],
|
||||
"requirements": ["python-homeassistant-analytics==0.6.0"],
|
||||
"requirements": ["python-homeassistant-analytics==0.7.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
},
|
||||
"learn_sendevent": {
|
||||
"name": "Learn sendevent",
|
||||
"description": "Translates a key press on a remote into ADB 'sendevent' commands. You must press one button on the remote within 8 seconds of calling this service."
|
||||
"description": "Translates a key press on a remote into ADB 'sendevent' commands. You must press one button on the remote within 8 seconds of performing this action."
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioaquacell"],
|
||||
"requirements": ["aioaquacell==0.1.8"]
|
||||
"requirements": ["aioaquacell==0.2.0"]
|
||||
}
|
||||
|
||||
@@ -28,5 +28,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/august",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["pubnub", "yalexs"],
|
||||
"requirements": ["yalexs==6.4.2", "yalexs-ble==2.4.3"]
|
||||
"requirements": ["yalexs==6.4.3", "yalexs-ble==2.4.3"]
|
||||
}
|
||||
|
||||
49
homeassistant/components/autarco/__init__.py
Normal file
49
homeassistant/components/autarco/__init__.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""The Autarco integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from autarco import Autarco
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
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 AutarcoDataUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SENSOR]
|
||||
|
||||
type AutarcoConfigEntry = ConfigEntry[list[AutarcoDataUpdateCoordinator]]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AutarcoConfigEntry) -> bool:
|
||||
"""Set up Autarco from a config entry."""
|
||||
client = Autarco(
|
||||
email=entry.data[CONF_EMAIL],
|
||||
password=entry.data[CONF_PASSWORD],
|
||||
session=async_get_clientsession(hass),
|
||||
)
|
||||
account_sites = await client.get_account()
|
||||
|
||||
coordinators: list[AutarcoDataUpdateCoordinator] = [
|
||||
AutarcoDataUpdateCoordinator(hass, client, site) for site in account_sites
|
||||
]
|
||||
|
||||
await asyncio.gather(
|
||||
*[
|
||||
coordinator.async_config_entry_first_refresh()
|
||||
for coordinator in coordinators
|
||||
]
|
||||
)
|
||||
|
||||
entry.runtime_data = coordinators
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: AutarcoConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
57
homeassistant/components/autarco/config_flow.py
Normal file
57
homeassistant/components/autarco/config_flow.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""Config flow for Autarco integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from autarco import Autarco, AutarcoAuthenticationError, AutarcoConnectionError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_EMAIL): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class AutarcoConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Autarco."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match({CONF_EMAIL: user_input[CONF_EMAIL]})
|
||||
client = Autarco(
|
||||
email=user_input[CONF_EMAIL],
|
||||
password=user_input[CONF_PASSWORD],
|
||||
session=async_get_clientsession(self.hass),
|
||||
)
|
||||
try:
|
||||
await client.get_account()
|
||||
except AutarcoAuthenticationError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except AutarcoConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_EMAIL],
|
||||
data={
|
||||
CONF_EMAIL: user_input[CONF_EMAIL],
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
},
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
errors=errors,
|
||||
data_schema=DATA_SCHEMA,
|
||||
)
|
||||
11
homeassistant/components/autarco/const.py
Normal file
11
homeassistant/components/autarco/const.py
Normal file
@@ -0,0 +1,11 @@
|
||||
"""Constants for the Autarco integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Final
|
||||
|
||||
DOMAIN: Final = "autarco"
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
SCAN_INTERVAL = timedelta(minutes=5)
|
||||
49
homeassistant/components/autarco/coordinator.py
Normal file
49
homeassistant/components/autarco/coordinator.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""Coordinator for Autarco integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple
|
||||
|
||||
from autarco import AccountSite, Autarco, Inverter, Solar
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN, LOGGER, SCAN_INTERVAL
|
||||
|
||||
|
||||
class AutarcoData(NamedTuple):
|
||||
"""Class for defining data in dict."""
|
||||
|
||||
solar: Solar
|
||||
inverters: dict[str, Inverter]
|
||||
|
||||
|
||||
class AutarcoDataUpdateCoordinator(DataUpdateCoordinator[AutarcoData]):
|
||||
"""Class to manage fetching Autarco data from the API."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
client: Autarco,
|
||||
site: AccountSite,
|
||||
) -> None:
|
||||
"""Initialize global Autarco data updater."""
|
||||
super().__init__(
|
||||
hass,
|
||||
LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
self.client = client
|
||||
self.site = site
|
||||
|
||||
async def _async_update_data(self) -> AutarcoData:
|
||||
"""Fetch data from Autarco API."""
|
||||
return AutarcoData(
|
||||
solar=await self.client.get_solar(self.site.public_key),
|
||||
inverters=await self.client.get_inverters(self.site.public_key),
|
||||
)
|
||||
43
homeassistant/components/autarco/diagnostics.py
Normal file
43
homeassistant/components/autarco/diagnostics.py
Normal file
@@ -0,0 +1,43 @@
|
||||
"""Support for the Autarco diagnostics."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import AutarcoConfigEntry, AutarcoDataUpdateCoordinator
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, config_entry: AutarcoConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
autarco_data: list[AutarcoDataUpdateCoordinator] = config_entry.runtime_data
|
||||
|
||||
return {
|
||||
"sites_data": [
|
||||
{
|
||||
"id": coordinator.site.site_id,
|
||||
"name": coordinator.site.system_name,
|
||||
"health": coordinator.site.health,
|
||||
"solar": {
|
||||
"power_production": coordinator.data.solar.power_production,
|
||||
"energy_production_today": coordinator.data.solar.energy_production_today,
|
||||
"energy_production_month": coordinator.data.solar.energy_production_month,
|
||||
"energy_production_total": coordinator.data.solar.energy_production_total,
|
||||
},
|
||||
"inverters": [
|
||||
{
|
||||
"serial_number": inverter.serial_number,
|
||||
"out_ac_power": inverter.out_ac_power,
|
||||
"out_ac_energy_total": inverter.out_ac_energy_total,
|
||||
"grid_turned_off": inverter.grid_turned_off,
|
||||
"health": inverter.health,
|
||||
}
|
||||
for inverter in coordinator.data.inverters.values()
|
||||
],
|
||||
}
|
||||
for coordinator in autarco_data
|
||||
],
|
||||
}
|
||||
9
homeassistant/components/autarco/manifest.json
Normal file
9
homeassistant/components/autarco/manifest.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"domain": "autarco",
|
||||
"name": "Autarco",
|
||||
"codeowners": ["@klaasnicolaas"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/autarco",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["autarco==2.0.0"]
|
||||
}
|
||||
189
homeassistant/components/autarco/sensor.py
Normal file
189
homeassistant/components/autarco/sensor.py
Normal file
@@ -0,0 +1,189 @@
|
||||
"""Support for Autarco sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from autarco import Inverter, Solar
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import UnitOfEnergy, UnitOfPower
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import AutarcoConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .coordinator import AutarcoDataUpdateCoordinator
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AutarcoSolarSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes an Autarco sensor entity."""
|
||||
|
||||
value_fn: Callable[[Solar], StateType]
|
||||
|
||||
|
||||
SENSORS_SOLAR: tuple[AutarcoSolarSensorEntityDescription, ...] = (
|
||||
AutarcoSolarSensorEntityDescription(
|
||||
key="power_production",
|
||||
translation_key="power_production",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda solar: solar.power_production,
|
||||
),
|
||||
AutarcoSolarSensorEntityDescription(
|
||||
key="energy_production_today",
|
||||
translation_key="energy_production_today",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
value_fn=lambda solar: solar.energy_production_today,
|
||||
),
|
||||
AutarcoSolarSensorEntityDescription(
|
||||
key="energy_production_month",
|
||||
translation_key="energy_production_month",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
value_fn=lambda solar: solar.energy_production_month,
|
||||
),
|
||||
AutarcoSolarSensorEntityDescription(
|
||||
key="energy_production_total",
|
||||
translation_key="energy_production_total",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda solar: solar.energy_production_total,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AutarcoInverterSensorEntityDescription(SensorEntityDescription):
|
||||
"""Describes an Autarco inverter sensor entity."""
|
||||
|
||||
value_fn: Callable[[Inverter], StateType]
|
||||
|
||||
|
||||
SENSORS_INVERTER: tuple[AutarcoInverterSensorEntityDescription, ...] = (
|
||||
AutarcoInverterSensorEntityDescription(
|
||||
key="out_ac_power",
|
||||
translation_key="out_ac_power",
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda inverter: inverter.out_ac_power,
|
||||
),
|
||||
AutarcoInverterSensorEntityDescription(
|
||||
key="out_ac_energy_total",
|
||||
translation_key="out_ac_energy_total",
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
value_fn=lambda inverter: inverter.out_ac_energy_total,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: AutarcoConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Autarco sensors based on a config entry."""
|
||||
entities: list[SensorEntity] = []
|
||||
for coordinator in entry.runtime_data:
|
||||
entities.extend(
|
||||
AutarcoSolarSensorEntity(
|
||||
coordinator=coordinator,
|
||||
description=description,
|
||||
)
|
||||
for description in SENSORS_SOLAR
|
||||
)
|
||||
entities.extend(
|
||||
AutarcoInverterSensorEntity(
|
||||
coordinator=coordinator,
|
||||
description=description,
|
||||
serial_number=inverter,
|
||||
)
|
||||
for description in SENSORS_INVERTER
|
||||
for inverter in coordinator.data.inverters
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AutarcoSolarSensorEntity(
|
||||
CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity
|
||||
):
|
||||
"""Defines an Autarco solar sensor."""
|
||||
|
||||
entity_description: AutarcoSolarSensorEntityDescription
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
coordinator: AutarcoDataUpdateCoordinator,
|
||||
description: AutarcoSolarSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize Autarco sensor."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.site.site_id}_solar_{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, f"{coordinator.site.site_id}_solar")},
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
manufacturer="Autarco",
|
||||
name="Solar",
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data.solar)
|
||||
|
||||
|
||||
class AutarcoInverterSensorEntity(
|
||||
CoordinatorEntity[AutarcoDataUpdateCoordinator], SensorEntity
|
||||
):
|
||||
"""Defines an Autarco inverter sensor."""
|
||||
|
||||
entity_description: AutarcoInverterSensorEntityDescription
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
coordinator: AutarcoDataUpdateCoordinator,
|
||||
description: AutarcoInverterSensorEntityDescription,
|
||||
serial_number: str,
|
||||
) -> None:
|
||||
"""Initialize Autarco sensor."""
|
||||
super().__init__(coordinator)
|
||||
|
||||
self.entity_description = description
|
||||
self._serial_number = serial_number
|
||||
self._attr_unique_id = f"{serial_number}_{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, serial_number)},
|
||||
name=f"Inverter {serial_number}",
|
||||
manufacturer="Autarco",
|
||||
model="Inverter",
|
||||
serial_number=serial_number,
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(
|
||||
self.coordinator.data.inverters[self._serial_number]
|
||||
)
|
||||
46
homeassistant/components/autarco/strings.json
Normal file
46
homeassistant/components/autarco/strings.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Connect to your Autarco account to get information about your solar panels.",
|
||||
"data": {
|
||||
"email": "[%key:common::config_flow::data::email%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"email": "The email address of your Autarco account.",
|
||||
"password": "The password of your Autarco account."
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"power_production": {
|
||||
"name": "Power production"
|
||||
},
|
||||
"energy_production_today": {
|
||||
"name": "Energy production today"
|
||||
},
|
||||
"energy_production_month": {
|
||||
"name": "Energy production month"
|
||||
},
|
||||
"energy_production_total": {
|
||||
"name": "Energy production total"
|
||||
},
|
||||
"out_ac_power": {
|
||||
"name": "Power AC output"
|
||||
},
|
||||
"out_ac_energy_total": {
|
||||
"name": "Energy AC output total"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,12 +37,12 @@
|
||||
},
|
||||
"issues": {
|
||||
"service_not_found": {
|
||||
"title": "{name} uses an unknown service",
|
||||
"title": "{name} uses an unknown action",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "[%key:component::automation::issues::service_not_found::title%]",
|
||||
"description": "The automation \"{name}\" (`{entity_id}`) has an action that calls an unknown service: `{service}`.\n\nThis error prevents the automation from running correctly. Maybe this service is no longer available, or perhaps a typo caused it.\n\nTo fix this error, [edit the automation]({edit}) and remove the action that calls this service.\n\nClick on SUBMIT below to confirm you have fixed this automation."
|
||||
"description": "The automation \"{name}\" (`{entity_id}`) has an unknown action: `{service}`.\n\nThis error prevents the automation from running correctly. Maybe this action is no longer available, or perhaps a typo caused it.\n\nTo fix this error, [edit the automation]({edit}) and remove the action that calls this action.\n\nClick on SUBMIT below to confirm you have fixed this automation."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/aws",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiobotocore", "botocore"],
|
||||
"requirements": ["aiobotocore==2.13.0"]
|
||||
"requirements": ["aiobotocore==2.13.1", "botocore==1.34.131"]
|
||||
}
|
||||
|
||||
@@ -65,13 +65,18 @@ class AzureDataExplorerClient:
|
||||
)
|
||||
|
||||
if data[CONF_USE_QUEUED_CLIENT] is True:
|
||||
# Queded is the only option supported on free tear of ADX
|
||||
# Queued is the only option supported on free tier of ADX
|
||||
self.write_client = QueuedIngestClient(kcsb_ingest)
|
||||
else:
|
||||
self.write_client = ManagedStreamingIngestClient.from_dm_kcsb(kcsb_ingest)
|
||||
|
||||
self.query_client = KustoClient(kcsb_query)
|
||||
|
||||
# Reduce the HTTP logging, the default INFO logging is too verbose.
|
||||
logging.getLogger("azure.core.pipeline.policies.http_logging_policy").setLevel(
|
||||
logging.WARNING
|
||||
)
|
||||
|
||||
def test_connection(self) -> None:
|
||||
"""Test connection, will throw Exception if it cannot connect."""
|
||||
|
||||
@@ -80,7 +85,7 @@ class AzureDataExplorerClient:
|
||||
self.query_client.execute_query(self._database, query)
|
||||
|
||||
def ingest_data(self, adx_events: str) -> None:
|
||||
"""Send data to Axure Data Explorer."""
|
||||
"""Send data to Azure Data Explorer."""
|
||||
|
||||
bytes_stream = io.StringIO(adx_events)
|
||||
stream_descriptor = StreamDescriptor(bytes_stream)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"name": "Bayesian",
|
||||
"codeowners": ["@HarvsG"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/bayesian",
|
||||
"integration_type": "helper",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
|
||||
@@ -4,14 +4,13 @@ from __future__ import annotations
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import ATTR_DEVICE_ID, CONF_PIN
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .const import ATTR_CONFIG_ENTRY_ID, DOMAIN, SERVICE_SEND_PIN
|
||||
from .coordinator import BlinkUpdateCoordinator
|
||||
|
||||
SERVICE_UPDATE_SCHEMA = vol.Schema(
|
||||
{
|
||||
@@ -29,45 +28,6 @@ SERVICE_SEND_PIN_SCHEMA = vol.Schema(
|
||||
def setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the services for the Blink integration."""
|
||||
|
||||
def collect_coordinators(
|
||||
device_ids: list[str],
|
||||
) -> list[BlinkUpdateCoordinator]:
|
||||
config_entries: list[ConfigEntry] = []
|
||||
registry = dr.async_get(hass)
|
||||
for target in device_ids:
|
||||
device = registry.async_get(target)
|
||||
if device:
|
||||
device_entries: list[ConfigEntry] = []
|
||||
for entry_id in device.config_entries:
|
||||
entry = hass.config_entries.async_get_entry(entry_id)
|
||||
if entry and entry.domain == DOMAIN:
|
||||
device_entries.append(entry)
|
||||
if not device_entries:
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="invalid_device",
|
||||
translation_placeholders={"target": target, "domain": DOMAIN},
|
||||
)
|
||||
config_entries.extend(device_entries)
|
||||
else:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="device_not_found",
|
||||
translation_placeholders={"target": target},
|
||||
)
|
||||
|
||||
coordinators: list[BlinkUpdateCoordinator] = []
|
||||
for config_entry in config_entries:
|
||||
if config_entry.state != ConfigEntryState.LOADED:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="not_loaded",
|
||||
translation_placeholders={"target": config_entry.title},
|
||||
)
|
||||
|
||||
coordinators.append(hass.data[DOMAIN][config_entry.entry_id])
|
||||
return coordinators
|
||||
|
||||
async def send_pin(call: ServiceCall):
|
||||
"""Call blink to send new pin."""
|
||||
for entry_id in call.data[ATTR_CONFIG_ENTRY_ID]:
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"""Support for Blinkstick lights."""
|
||||
|
||||
# mypy: ignore-errors
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from blinkstick import blinkstick
|
||||
# from blinkstick import blinkstick
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.light import (
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"domain": "blinksticklight",
|
||||
"name": "BlinkStick",
|
||||
"codeowners": [],
|
||||
"disabled": "This integration is disabled because it uses non-open source code to operate.",
|
||||
"documentation": "https://www.home-assistant.io/integrations/blinksticklight",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["blinkstick"],
|
||||
|
||||
5
homeassistant/components/blinksticklight/ruff.toml
Normal file
5
homeassistant/components/blinksticklight/ruff.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
extend = "../../../pyproject.toml"
|
||||
|
||||
lint.extend-ignore = [
|
||||
"F821"
|
||||
]
|
||||
@@ -131,7 +131,7 @@ SENSOR_TYPES: list[BMWSensorEntityDescription] = [
|
||||
BMWSensorEntityDescription(
|
||||
key="fuel_and_battery.remaining_fuel",
|
||||
translation_key="remaining_fuel",
|
||||
device_class=SensorDeviceClass.VOLUME,
|
||||
device_class=SensorDeviceClass.VOLUME_STORAGE,
|
||||
native_unit_of_measurement=UnitOfVolume.LITERS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
"message": "Authentication failed for {email}, check your email and password"
|
||||
},
|
||||
"notify_missing_argument_item": {
|
||||
"message": "Failed to call service {service}. 'URGENT_MESSAGE' requires a value @ data['item']. Got None"
|
||||
"message": "Failed to perform action {service}. 'URGENT_MESSAGE' requires a value @ data['item']. Got None"
|
||||
},
|
||||
"notify_request_failed": {
|
||||
"message": "Failed to send push notification for bring due to a connection error, try again later"
|
||||
|
||||
@@ -6,7 +6,9 @@ DOMAIN = "broadlink"
|
||||
|
||||
DOMAINS_AND_TYPES = {
|
||||
Platform.CLIMATE: {"HYS"},
|
||||
Platform.LIGHT: {"LB1", "LB2"},
|
||||
Platform.REMOTE: {"RM4MINI", "RM4PRO", "RMMINI", "RMMINIB", "RMPRO"},
|
||||
Platform.SELECT: {"HYS"},
|
||||
Platform.SENSOR: {
|
||||
"A1",
|
||||
"MP1S",
|
||||
@@ -35,7 +37,7 @@ DOMAINS_AND_TYPES = {
|
||||
"SP4",
|
||||
"SP4B",
|
||||
},
|
||||
Platform.LIGHT: {"LB1", "LB2"},
|
||||
Platform.TIME: {"HYS"},
|
||||
}
|
||||
DEVICE_TYPES = set.union(*DOMAINS_AND_TYPES.values())
|
||||
|
||||
|
||||
69
homeassistant/components/broadlink/select.py
Normal file
69
homeassistant/components/broadlink/select.py
Normal file
@@ -0,0 +1,69 @@
|
||||
"""Support for Broadlink selects."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.select import SelectEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import BroadlinkDevice
|
||||
from .const import DOMAIN
|
||||
from .entity import BroadlinkEntity
|
||||
|
||||
DAY_ID_TO_NAME = {
|
||||
1: "monday",
|
||||
2: "tuesday",
|
||||
3: "wednesday",
|
||||
4: "thursday",
|
||||
5: "friday",
|
||||
6: "saturday",
|
||||
7: "sunday",
|
||||
}
|
||||
DAY_NAME_TO_ID = {v: k for k, v in DAY_ID_TO_NAME.items()}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Broadlink select."""
|
||||
device = hass.data[DOMAIN].devices[config_entry.entry_id]
|
||||
async_add_entities([BroadlinkDayOfWeek(device)])
|
||||
|
||||
|
||||
class BroadlinkDayOfWeek(BroadlinkEntity, SelectEntity):
|
||||
"""Representation of a Broadlink day of week."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_current_option: str | None = None
|
||||
_attr_options = list(DAY_NAME_TO_ID)
|
||||
_attr_translation_key = "day_of_week"
|
||||
|
||||
def __init__(self, device: BroadlinkDevice) -> None:
|
||||
"""Initialize the select."""
|
||||
super().__init__(device)
|
||||
|
||||
self._attr_unique_id = f"{device.unique_id}-dayofweek"
|
||||
|
||||
def _update_state(self, data: dict[str, Any]) -> None:
|
||||
"""Update the state of the entity."""
|
||||
if data is None or "dayofweek" not in data:
|
||||
self._attr_current_option = None
|
||||
else:
|
||||
self._attr_current_option = DAY_ID_TO_NAME[data["dayofweek"]]
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
await self._device.async_request(
|
||||
self._device.api.set_time,
|
||||
hour=self._coordinator.data["hour"],
|
||||
minute=self._coordinator.data["min"],
|
||||
second=self._coordinator.data["sec"],
|
||||
day=DAY_NAME_TO_ID[option],
|
||||
)
|
||||
self._attr_current_option = option
|
||||
self.async_write_ha_state()
|
||||
@@ -61,6 +61,20 @@
|
||||
"total_consumption": {
|
||||
"name": "Total consumption"
|
||||
}
|
||||
},
|
||||
"select": {
|
||||
"day_of_week": {
|
||||
"name": "Day of week",
|
||||
"state": {
|
||||
"monday": "[%key:common::time::monday%]",
|
||||
"tuesday": "[%key:common::time::tuesday%]",
|
||||
"wednesday": "[%key:common::time::wednesday%]",
|
||||
"thursday": "[%key:common::time::thursday%]",
|
||||
"friday": "[%key:common::time::friday%]",
|
||||
"saturday": "[%key:common::time::saturday%]",
|
||||
"sunday": "[%key:common::time::sunday%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
63
homeassistant/components/broadlink/time.py
Normal file
63
homeassistant/components/broadlink/time.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""Support for Broadlink device time."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import time
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.time import TimeEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from . import BroadlinkDevice
|
||||
from .const import DOMAIN
|
||||
from .entity import BroadlinkEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the Broadlink time."""
|
||||
device = hass.data[DOMAIN].devices[config_entry.entry_id]
|
||||
async_add_entities([BroadlinkTime(device)])
|
||||
|
||||
|
||||
class BroadlinkTime(BroadlinkEntity, TimeEntity):
|
||||
"""Representation of a Broadlink device time."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
_attr_native_value: time | None = None
|
||||
|
||||
def __init__(self, device: BroadlinkDevice) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(device)
|
||||
|
||||
self._attr_unique_id = f"{device.unique_id}-device_time"
|
||||
|
||||
def _update_state(self, data: dict[str, Any]) -> None:
|
||||
"""Update the state of the entity."""
|
||||
if data is None or "hour" not in data or "min" not in data or "sec" not in data:
|
||||
self._attr_native_value = None
|
||||
else:
|
||||
self._attr_native_value = time(
|
||||
hour=data["hour"],
|
||||
minute=data["min"],
|
||||
second=data["sec"],
|
||||
tzinfo=dt_util.get_default_time_zone(),
|
||||
)
|
||||
|
||||
async def async_set_value(self, value: time) -> None:
|
||||
"""Change the value."""
|
||||
await self._device.async_request(
|
||||
self._device.api.set_time,
|
||||
hour=value.hour,
|
||||
minute=value.minute,
|
||||
second=value.second,
|
||||
day=self._coordinator.data["dayofweek"],
|
||||
)
|
||||
self._attr_native_value = value
|
||||
self.async_write_ha_state()
|
||||
@@ -111,12 +111,12 @@
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_service_calendar_list_events": {
|
||||
"title": "Detected use of deprecated service `calendar.list_events`",
|
||||
"title": "Detected use of deprecated action `calendar.list_events`",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"title": "[%key:component::calendar::issues::deprecated_service_calendar_list_events::title%]",
|
||||
"description": "Use `calendar.get_events` instead which supports multiple entities.\n\nPlease replace this service and adjust your automations and scripts and select **submit** to close this issue."
|
||||
"description": "Use `calendar.get_events` instead which supports multiple entities.\n\nPlease replace this action and adjust your automations and scripts and select **submit** to close this issue."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"""Support for Concord232 alarm control panels."""
|
||||
|
||||
# mypy: ignore-errors
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from concord232 import client as concord232_client
|
||||
# from concord232 import client as concord232_client
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"""Support for exposing Concord232 elements as sensors."""
|
||||
|
||||
# mypy: ignore-errors
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
from concord232 import client as concord232_client
|
||||
# from concord232 import client as concord232_client
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"domain": "concord232",
|
||||
"name": "Concord232",
|
||||
"codeowners": [],
|
||||
"disabled": "This integration is disabled because it uses non-open source code to operate.",
|
||||
"documentation": "https://www.home-assistant.io/integrations/concord232",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["concord232", "stevedore"],
|
||||
|
||||
5
homeassistant/components/concord232/ruff.toml
Normal file
5
homeassistant/components/concord232/ruff.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
extend = "../../../pyproject.toml"
|
||||
|
||||
lint.extend-ignore = [
|
||||
"F821"
|
||||
]
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/conversation",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["hassil==1.7.3", "home-assistant-intents==2024.7.3"]
|
||||
"requirements": ["hassil==1.7.4", "home-assistant-intents==2024.7.10"]
|
||||
}
|
||||
|
||||
@@ -34,4 +34,4 @@ def create_matcher(utterance: str) -> re.Pattern[str]:
|
||||
pattern.append(rf"(?:{optional_match.groups()[0]} *)?")
|
||||
|
||||
pattern.append("$")
|
||||
return re.compile("".join(pattern), re.I)
|
||||
return re.compile("".join(pattern), re.IGNORECASE)
|
||||
|
||||
@@ -3,11 +3,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from doorbirdpy import DoorBird
|
||||
import requests
|
||||
|
||||
from homeassistant.components import persistent_notification
|
||||
from homeassistant.const import (
|
||||
@@ -18,7 +16,8 @@ from homeassistant.const import (
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
@@ -27,8 +26,6 @@ from .device import ConfiguredDoorBird
|
||||
from .models import DoorBirdConfigEntry, DoorBirdData
|
||||
from .view import DoorBirdRequestView
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_CUSTOM_URL = "hass_url_override"
|
||||
|
||||
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
|
||||
@@ -48,36 +45,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: DoorBirdConfigEntry) ->
|
||||
device_ip = door_station_config[CONF_HOST]
|
||||
username = door_station_config[CONF_USERNAME]
|
||||
password = door_station_config[CONF_PASSWORD]
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
device = DoorBird(device_ip, username, password)
|
||||
device = DoorBird(device_ip, username, password, http_session=session)
|
||||
try:
|
||||
status, info = await hass.async_add_executor_job(_init_door_bird_device, device)
|
||||
except requests.exceptions.HTTPError as err:
|
||||
if err.response.status_code == HTTPStatus.UNAUTHORIZED:
|
||||
_LOGGER.error(
|
||||
"Authorization rejected by DoorBird for %s@%s", username, device_ip
|
||||
)
|
||||
return False
|
||||
info = await device.info()
|
||||
except ClientResponseError as err:
|
||||
if err.status == HTTPStatus.UNAUTHORIZED:
|
||||
raise ConfigEntryAuthFailed from err
|
||||
raise ConfigEntryNotReady from err
|
||||
except OSError as oserr:
|
||||
_LOGGER.error("Failed to setup doorbird at %s: %s", device_ip, oserr)
|
||||
raise ConfigEntryNotReady from oserr
|
||||
|
||||
if not status[0]:
|
||||
_LOGGER.error(
|
||||
"Could not connect to DoorBird as %s@%s: Error %s",
|
||||
username,
|
||||
device_ip,
|
||||
str(status[1]),
|
||||
)
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
token: str = door_station_config.get(CONF_TOKEN, config_entry_id)
|
||||
custom_url: str | None = door_station_config.get(CONF_CUSTOM_URL)
|
||||
name: str | None = door_station_config.get(CONF_NAME)
|
||||
events = entry.options.get(CONF_EVENTS, [])
|
||||
event_entity_ids: dict[str, str] = {}
|
||||
door_station = ConfiguredDoorBird(device, name, custom_url, token, event_entity_ids)
|
||||
door_station = ConfiguredDoorBird(
|
||||
hass, device, name, custom_url, token, event_entity_ids
|
||||
)
|
||||
door_bird_data = DoorBirdData(door_station, info, event_entity_ids)
|
||||
door_station.update_events(events)
|
||||
# Subscribe to doorbell or motion events
|
||||
@@ -91,11 +78,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: DoorBirdConfigEntry) ->
|
||||
return True
|
||||
|
||||
|
||||
def _init_door_bird_device(device: DoorBird) -> tuple[tuple[bool, int], dict[str, Any]]:
|
||||
"""Verify we can connect to the device and return the status."""
|
||||
return device.ready(), device.info()
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: DoorBirdConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
@@ -106,8 +88,8 @@ async def _async_register_events(
|
||||
) -> bool:
|
||||
"""Register events on device."""
|
||||
try:
|
||||
await hass.async_add_executor_job(door_station.register_events, hass)
|
||||
except requests.exceptions.HTTPError:
|
||||
await door_station.async_register_events()
|
||||
except ClientResponseError:
|
||||
persistent_notification.async_create(
|
||||
hass,
|
||||
(
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
"""Support for powering relays in a DoorBird video doorbell."""
|
||||
"""Support for relays and actions in a DoorBird video doorbell."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from doorbirdpy import DoorBird
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass, replace
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .device import ConfiguredDoorBird, async_reset_device_favorites
|
||||
from .entity import DoorBirdEntity
|
||||
from .models import DoorBirdConfigEntry, DoorBirdData
|
||||
|
||||
@@ -19,18 +20,25 @@ IR_RELAY = "__ir_light__"
|
||||
class DoorbirdButtonEntityDescription(ButtonEntityDescription):
|
||||
"""Class to describe a Doorbird Button entity."""
|
||||
|
||||
press_action: Callable[[DoorBird, str], None]
|
||||
press_action: Callable[[ConfiguredDoorBird, str], Coroutine[Any, Any, bool | None]]
|
||||
|
||||
|
||||
RELAY_ENTITY_DESCRIPTION = DoorbirdButtonEntityDescription(
|
||||
key="relay",
|
||||
translation_key="relay",
|
||||
press_action=lambda device, relay: device.energize_relay(relay),
|
||||
press_action=lambda door_station, relay: door_station.device.energize_relay(relay),
|
||||
)
|
||||
IR_ENTITY_DESCRIPTION = DoorbirdButtonEntityDescription(
|
||||
key="ir",
|
||||
translation_key="ir",
|
||||
press_action=lambda device, _: device.turn_light_on(),
|
||||
BUTTON_DESCRIPTIONS: tuple[DoorbirdButtonEntityDescription, ...] = (
|
||||
DoorbirdButtonEntityDescription(
|
||||
key="__ir_light__",
|
||||
translation_key="ir",
|
||||
press_action=lambda door_station, _: door_station.device.turn_light_on(),
|
||||
),
|
||||
DoorbirdButtonEntityDescription(
|
||||
key="reset_favorites",
|
||||
translation_key="reset_favorites",
|
||||
press_action=lambda door_station, _: async_reset_device_favorites(door_station),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -41,38 +49,39 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the DoorBird button platform."""
|
||||
door_bird_data = config_entry.runtime_data
|
||||
relays = door_bird_data.door_station_info["RELAYS"]
|
||||
|
||||
relays: list[str] = door_bird_data.door_station_info["RELAYS"]
|
||||
entities = [
|
||||
DoorBirdButton(door_bird_data, relay, RELAY_ENTITY_DESCRIPTION)
|
||||
DoorBirdButton(
|
||||
door_bird_data,
|
||||
replace(RELAY_ENTITY_DESCRIPTION, name=f"Relay {relay}"),
|
||||
relay,
|
||||
)
|
||||
for relay in relays
|
||||
]
|
||||
entities.append(DoorBirdButton(door_bird_data, IR_RELAY, IR_ENTITY_DESCRIPTION))
|
||||
|
||||
entities.extend(
|
||||
DoorBirdButton(door_bird_data, button_description)
|
||||
for button_description in BUTTON_DESCRIPTIONS
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class DoorBirdButton(DoorBirdEntity, ButtonEntity):
|
||||
"""A relay in a DoorBird device."""
|
||||
"""A button for a DoorBird device."""
|
||||
|
||||
entity_description: DoorbirdButtonEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
door_bird_data: DoorBirdData,
|
||||
relay: str,
|
||||
entity_description: DoorbirdButtonEntityDescription,
|
||||
relay: str | None = None,
|
||||
) -> None:
|
||||
"""Initialize a relay in a DoorBird device."""
|
||||
"""Initialize a button for a DoorBird device."""
|
||||
super().__init__(door_bird_data)
|
||||
self._relay = relay
|
||||
self._relay = relay or ""
|
||||
self.entity_description = entity_description
|
||||
if self._relay == IR_RELAY:
|
||||
self._attr_name = "IR"
|
||||
else:
|
||||
self._attr_name = f"Relay {self._relay}"
|
||||
self._attr_unique_id = f"{self._mac_addr}_{self._relay}"
|
||||
self._attr_unique_id = f"{self._mac_addr}_{relay or entity_description.key}"
|
||||
|
||||
def press(self) -> None:
|
||||
"""Power the relay."""
|
||||
self.entity_description.press_action(self._door_station.device, self._relay)
|
||||
async def async_press(self) -> None:
|
||||
"""Call the press action."""
|
||||
await self.entity_description.press_action(self._door_station, self._relay)
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
@@ -10,7 +9,6 @@ import aiohttp
|
||||
|
||||
from homeassistant.components.camera import Camera, CameraEntityFeature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
@@ -95,11 +93,9 @@ class DoorBirdCamera(DoorBirdEntity, Camera):
|
||||
return self._last_image
|
||||
|
||||
try:
|
||||
websession = async_get_clientsession(self.hass)
|
||||
async with asyncio.timeout(_TIMEOUT):
|
||||
response = await websession.get(self._url)
|
||||
|
||||
self._last_image = await response.read()
|
||||
self._last_image = await self._door_station.device.get_image(
|
||||
self._url, timeout=_TIMEOUT
|
||||
)
|
||||
except TimeoutError:
|
||||
_LOGGER.error("DoorBird %s: Camera image timed out", self.name)
|
||||
return self._last_image
|
||||
|
||||
@@ -2,12 +2,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from http import HTTPStatus
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientResponseError
|
||||
from doorbirdpy import DoorBird
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import zeroconf
|
||||
@@ -20,12 +21,29 @@ from homeassistant.config_entries import (
|
||||
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
|
||||
from .const import CONF_EVENTS, DOMAIN, DOORBIRD_OUI
|
||||
from .const import (
|
||||
CONF_EVENTS,
|
||||
DEFAULT_DOORBELL_EVENT,
|
||||
DEFAULT_MOTION_EVENT,
|
||||
DOMAIN,
|
||||
DOORBIRD_OUI,
|
||||
)
|
||||
from .util import get_mac_address_from_door_station_info
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_OPTIONS = {CONF_EVENTS: [DEFAULT_DOORBELL_EVENT, DEFAULT_MOTION_EVENT]}
|
||||
|
||||
|
||||
AUTH_VOL_DICT: VolDictType = {
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
AUTH_SCHEMA = vol.Schema(AUTH_VOL_DICT)
|
||||
|
||||
|
||||
def _schema_with_defaults(
|
||||
host: str | None = None, name: str | None = None
|
||||
@@ -33,33 +51,27 @@ def _schema_with_defaults(
|
||||
return vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_HOST, default=host): str,
|
||||
vol.Required(CONF_USERNAME): str,
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
**AUTH_VOL_DICT,
|
||||
vol.Optional(CONF_NAME, default=name): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _check_device(device: DoorBird) -> tuple[tuple[bool, int], dict[str, Any]]:
|
||||
"""Verify we can connect to the device and return the status."""
|
||||
return device.ready(), device.info()
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, str]:
|
||||
"""Validate the user input allows us to connect."""
|
||||
device = DoorBird(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD])
|
||||
session = async_get_clientsession(hass)
|
||||
device = DoorBird(
|
||||
data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD], http_session=session
|
||||
)
|
||||
try:
|
||||
status, info = await hass.async_add_executor_job(_check_device, device)
|
||||
except requests.exceptions.HTTPError as err:
|
||||
if err.response.status_code == HTTPStatus.UNAUTHORIZED:
|
||||
info = await device.info()
|
||||
except ClientResponseError as err:
|
||||
if err.status == HTTPStatus.UNAUTHORIZED:
|
||||
raise InvalidAuth from err
|
||||
raise CannotConnect from err
|
||||
except OSError as err:
|
||||
raise CannotConnect from err
|
||||
|
||||
if not status[0]:
|
||||
raise CannotConnect
|
||||
|
||||
mac_addr = get_mac_address_from_door_station_info(info)
|
||||
|
||||
# Return info that you want to store in the config entry.
|
||||
@@ -68,11 +80,12 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str,
|
||||
|
||||
async def async_verify_supported_device(hass: HomeAssistant, host: str) -> bool:
|
||||
"""Verify the doorbell state endpoint returns a 401."""
|
||||
device = DoorBird(host, "", "")
|
||||
session = async_get_clientsession(hass)
|
||||
device = DoorBird(host, "", "", http_session=session)
|
||||
try:
|
||||
await hass.async_add_executor_job(device.doorbell_state)
|
||||
except requests.exceptions.HTTPError as err:
|
||||
if err.response.status_code == HTTPStatus.UNAUTHORIZED:
|
||||
await device.doorbell_state()
|
||||
except ClientResponseError as err:
|
||||
if err.status == HTTPStatus.UNAUTHORIZED:
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
@@ -87,6 +100,47 @@ class DoorBirdConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the DoorBird config flow."""
|
||||
self.discovery_schema: vol.Schema | None = None
|
||||
self.reauth_entry: ConfigEntry | None = None
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauth."""
|
||||
entry_id = self.context["entry_id"]
|
||||
self.reauth_entry = self.hass.config_entries.async_get_entry(entry_id)
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauth input."""
|
||||
errors: dict[str, str] = {}
|
||||
existing_entry = self.reauth_entry
|
||||
assert existing_entry
|
||||
existing_data = existing_entry.data
|
||||
placeholders: dict[str, str] = {
|
||||
CONF_NAME: existing_data[CONF_NAME],
|
||||
CONF_HOST: existing_data[CONF_HOST],
|
||||
}
|
||||
self.context["title_placeholders"] = placeholders
|
||||
if user_input is not None:
|
||||
new_config = {
|
||||
**existing_data,
|
||||
CONF_USERNAME: user_input[CONF_USERNAME],
|
||||
CONF_PASSWORD: user_input[CONF_PASSWORD],
|
||||
}
|
||||
_, errors = await self._async_validate_or_error(new_config)
|
||||
if not errors:
|
||||
return self.async_update_reload_and_abort(
|
||||
existing_entry, data=new_config
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
description_placeholders=placeholders,
|
||||
step_id="reauth_confirm",
|
||||
data_schema=AUTH_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -98,7 +152,9 @@ class DoorBirdConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
if not errors:
|
||||
await self.async_set_unique_id(info["mac_addr"])
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(title=info["title"], data=user_input)
|
||||
return self.async_create_entry(
|
||||
title=info["title"], data=user_input, options=DEFAULT_OPTIONS
|
||||
)
|
||||
|
||||
data = self.discovery_schema or _schema_with_defaults()
|
||||
return self.async_show_form(step_id="user", data_schema=data, errors=errors)
|
||||
@@ -175,7 +231,6 @@ class OptionsFlowHandler(OptionsFlow):
|
||||
"""Handle options flow."""
|
||||
if user_input is not None:
|
||||
events = [event.strip() for event in user_input[CONF_EVENTS].split(",")]
|
||||
|
||||
return self.async_create_entry(title="", data={CONF_EVENTS: events})
|
||||
|
||||
current_events = self.config_entry.options.get(CONF_EVENTS, [])
|
||||
|
||||
@@ -4,9 +4,6 @@ from homeassistant.const import Platform
|
||||
|
||||
DOMAIN = "doorbird"
|
||||
PLATFORMS = [Platform.BUTTON, Platform.CAMERA, Platform.EVENT]
|
||||
DOOR_STATION = "door_station"
|
||||
DOOR_STATION_INFO = "door_station_info"
|
||||
DOOR_STATION_EVENT_ENTITY_IDS = "door_station_event_entity_ids"
|
||||
|
||||
CONF_EVENTS = "events"
|
||||
MANUFACTURER = "Bird Home Automation Group"
|
||||
@@ -22,3 +19,16 @@ DOORBIRD_INFO_KEY_WIFI_MAC_ADDR = "WIFI_MAC_ADDR"
|
||||
UNDO_UPDATE_LISTENER = "undo_update_listener"
|
||||
|
||||
API_URL = f"/api/{DOMAIN}"
|
||||
|
||||
|
||||
DEFAULT_DOORBELL_EVENT = "doorbell"
|
||||
DEFAULT_MOTION_EVENT = "motion"
|
||||
|
||||
DEFAULT_EVENT_TYPES = (
|
||||
(DEFAULT_DOORBELL_EVENT, "doorbell"),
|
||||
(DEFAULT_MOTION_EVENT, "motion"),
|
||||
)
|
||||
|
||||
HTTP_EVENT_TYPE = "http"
|
||||
MIN_WEEKDAY = 104400
|
||||
MAX_WEEKDAY = 104399
|
||||
|
||||
@@ -2,19 +2,31 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from functools import cached_property
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from doorbirdpy import DoorBird, DoorBirdScheduleEntry
|
||||
from doorbirdpy import (
|
||||
DoorBird,
|
||||
DoorBirdScheduleEntry,
|
||||
DoorBirdScheduleEntryOutput,
|
||||
DoorBirdScheduleEntrySchedule,
|
||||
)
|
||||
|
||||
from homeassistant.const import ATTR_ENTITY_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.network import get_url
|
||||
from homeassistant.util import dt as dt_util, slugify
|
||||
|
||||
from .const import API_URL
|
||||
from .const import (
|
||||
API_URL,
|
||||
DEFAULT_EVENT_TYPES,
|
||||
HTTP_EVENT_TYPE,
|
||||
MAX_WEEKDAY,
|
||||
MIN_WEEKDAY,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -27,11 +39,21 @@ class DoorbirdEvent:
|
||||
event_type: str
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class DoorbirdEventConfig:
|
||||
"""Describes the configuration of doorbird events."""
|
||||
|
||||
events: list[DoorbirdEvent]
|
||||
schedule: list[DoorBirdScheduleEntry]
|
||||
unconfigured_favorites: defaultdict[str, list[str]]
|
||||
|
||||
|
||||
class ConfiguredDoorBird:
|
||||
"""Attach additional information to pass along with configured device."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
device: DoorBird,
|
||||
name: str | None,
|
||||
custom_url: str | None,
|
||||
@@ -39,12 +61,15 @@ class ConfiguredDoorBird:
|
||||
event_entity_ids: dict[str, str],
|
||||
) -> None:
|
||||
"""Initialize configured device."""
|
||||
self._hass = hass
|
||||
self._name = name
|
||||
self._device = device
|
||||
self._custom_url = custom_url
|
||||
self._token = token
|
||||
self._event_entity_ids = event_entity_ids
|
||||
# Raw events, ie "doorbell" or "motion"
|
||||
self.events: list[str] = []
|
||||
# Event names, ie "doorbird_1234_doorbell" or "doorbird_1234_motion"
|
||||
self.door_station_events: list[str] = []
|
||||
self.event_descriptions: list[DoorbirdEvent] = []
|
||||
|
||||
@@ -75,35 +100,90 @@ class ConfiguredDoorBird:
|
||||
"""Get token for device."""
|
||||
return self._token
|
||||
|
||||
def register_events(self, hass: HomeAssistant) -> None:
|
||||
async def async_register_events(self) -> None:
|
||||
"""Register events on device."""
|
||||
if not self.door_station_events:
|
||||
# User may not have permission to get the favorites
|
||||
return
|
||||
|
||||
http_fav = await self._async_register_events()
|
||||
event_config = await self._async_get_event_config(http_fav)
|
||||
_LOGGER.debug("%s: Event config: %s", self.name, event_config)
|
||||
if event_config.unconfigured_favorites:
|
||||
await self._configure_unconfigured_favorites(event_config)
|
||||
event_config = await self._async_get_event_config(http_fav)
|
||||
self.event_descriptions = event_config.events
|
||||
|
||||
async def _configure_unconfigured_favorites(
|
||||
self, event_config: DoorbirdEventConfig
|
||||
) -> None:
|
||||
"""Configure unconfigured favorites."""
|
||||
for entry in event_config.schedule:
|
||||
modified_schedule = False
|
||||
for identifier in event_config.unconfigured_favorites.get(entry.input, ()):
|
||||
schedule = DoorBirdScheduleEntrySchedule()
|
||||
schedule.add_weekday(MIN_WEEKDAY, MAX_WEEKDAY)
|
||||
entry.output.append(
|
||||
DoorBirdScheduleEntryOutput(
|
||||
enabled=True,
|
||||
event=HTTP_EVENT_TYPE,
|
||||
param=identifier,
|
||||
schedule=schedule,
|
||||
)
|
||||
)
|
||||
modified_schedule = True
|
||||
|
||||
if modified_schedule:
|
||||
update_ok, code = await self.device.change_schedule(entry)
|
||||
if not update_ok:
|
||||
_LOGGER.error(
|
||||
"Unable to update schedule entry %s to %s. Error code: %s",
|
||||
self.name,
|
||||
entry.export,
|
||||
code,
|
||||
)
|
||||
|
||||
async def _async_register_events(self) -> dict[str, Any]:
|
||||
"""Register events on device."""
|
||||
# Override url if another is specified in the configuration
|
||||
if custom_url := self.custom_url:
|
||||
hass_url = custom_url
|
||||
else:
|
||||
# Get the URL of this server
|
||||
hass_url = get_url(hass, prefer_external=False)
|
||||
hass_url = get_url(self._hass, prefer_external=False)
|
||||
|
||||
if not self.door_station_events:
|
||||
# User may not have permission to get the favorites
|
||||
return
|
||||
http_fav = await self._async_get_http_favorites()
|
||||
if any(
|
||||
# Note that a list comp is used here to ensure all
|
||||
# events are registered and the any does not short circuit
|
||||
[
|
||||
await self._async_register_event(hass_url, event, http_fav)
|
||||
for event in self.door_station_events
|
||||
]
|
||||
):
|
||||
# If any events were registered, get the updated favorites
|
||||
http_fav = await self._async_get_http_favorites()
|
||||
|
||||
favorites = self.device.favorites()
|
||||
for event in self.door_station_events:
|
||||
if self._register_event(hass_url, event, favs=favorites):
|
||||
_LOGGER.info(
|
||||
"Successfully registered URL for %s on %s", event, self.name
|
||||
)
|
||||
return http_fav
|
||||
|
||||
schedule: list[DoorBirdScheduleEntry] = self.device.schedule()
|
||||
http_fav: dict[str, dict[str, Any]] = favorites.get("http") or {}
|
||||
favorite_input_type: dict[str, str] = {
|
||||
async def _async_get_event_config(
|
||||
self, http_fav: dict[str, dict[str, Any]]
|
||||
) -> DoorbirdEventConfig:
|
||||
"""Get events and unconfigured favorites from http favorites."""
|
||||
device = self.device
|
||||
schedule = await device.schedule()
|
||||
favorite_input_type = {
|
||||
output.param: entry.input
|
||||
for entry in schedule
|
||||
for output in entry.output
|
||||
if output.event == "http"
|
||||
if output.event == HTTP_EVENT_TYPE
|
||||
}
|
||||
events: list[DoorbirdEvent] = []
|
||||
unconfigured_favorites: defaultdict[str, list[str]] = defaultdict(list)
|
||||
default_event_types = {
|
||||
self._get_event_name(event): event_type
|
||||
for event, event_type in DEFAULT_EVENT_TYPES
|
||||
}
|
||||
for identifier, data in http_fav.items():
|
||||
title: str | None = data.get("title")
|
||||
if not title or not title.startswith("Home Assistant"):
|
||||
@@ -111,8 +191,10 @@ class ConfiguredDoorBird:
|
||||
event = title.split("(")[1].strip(")")
|
||||
if input_type := favorite_input_type.get(identifier):
|
||||
events.append(DoorbirdEvent(event, input_type))
|
||||
elif input_type := default_event_types.get(event):
|
||||
unconfigured_favorites[input_type].append(identifier)
|
||||
|
||||
self.event_descriptions = events
|
||||
return DoorbirdEventConfig(events, schedule, unconfigured_favorites)
|
||||
|
||||
@cached_property
|
||||
def slug(self) -> str:
|
||||
@@ -122,46 +204,38 @@ class ConfiguredDoorBird:
|
||||
def _get_event_name(self, event: str) -> str:
|
||||
return f"{self.slug}_{event}"
|
||||
|
||||
def _register_event(
|
||||
self, hass_url: str, event: str, favs: dict[str, Any] | None = None
|
||||
async def _async_get_http_favorites(self) -> dict[str, dict[str, Any]]:
|
||||
"""Get the HTTP favorites from the device."""
|
||||
return (await self.device.favorites()).get(HTTP_EVENT_TYPE) or {}
|
||||
|
||||
async def _async_register_event(
|
||||
self, hass_url: str, event: str, http_fav: dict[str, dict[str, Any]]
|
||||
) -> bool:
|
||||
"""Add a schedule entry in the device for a sensor."""
|
||||
"""Register an event.
|
||||
|
||||
Returns True if the event was registered, False if
|
||||
the event was already registered or registration failed.
|
||||
"""
|
||||
url = f"{hass_url}{API_URL}/{event}?token={self._token}"
|
||||
_LOGGER.debug("Registering URL %s for event %s", url, event)
|
||||
# If its already registered, don't register it again
|
||||
if any(fav["value"] == url for fav in http_fav.values()):
|
||||
_LOGGER.debug("URL already registered for %s", event)
|
||||
return False
|
||||
|
||||
# Register HA URL as webhook if not already, then get the ID
|
||||
if self.webhook_is_registered(url, favs=favs):
|
||||
return True
|
||||
|
||||
self.device.change_favorite("http", f"Home Assistant ({event})", url)
|
||||
if not self.webhook_is_registered(url):
|
||||
if not await self.device.change_favorite(
|
||||
HTTP_EVENT_TYPE, f"Home Assistant ({event})", url
|
||||
):
|
||||
_LOGGER.warning(
|
||||
'Unable to set favorite URL "%s". Event "%s" will not fire',
|
||||
url,
|
||||
event,
|
||||
)
|
||||
return False
|
||||
|
||||
_LOGGER.info("Successfully registered URL for %s on %s", event, self.name)
|
||||
return True
|
||||
|
||||
def webhook_is_registered(
|
||||
self, url: str, favs: dict[str, Any] | None = None
|
||||
) -> bool:
|
||||
"""Return whether the given URL is registered as a device favorite."""
|
||||
return self.get_webhook_id(url, favs) is not None
|
||||
|
||||
def get_webhook_id(
|
||||
self, url: str, favs: dict[str, Any] | None = None
|
||||
) -> str | None:
|
||||
"""Return the device favorite ID for the given URL.
|
||||
|
||||
The favorite must exist or there will be problems.
|
||||
"""
|
||||
favs = favs if favs else self.device.favorites()
|
||||
http_fav: dict[str, dict[str, Any]] = favs.get("http") or {}
|
||||
for fav_id, data in http_fav.items():
|
||||
if data["value"] == url:
|
||||
return fav_id
|
||||
return None
|
||||
|
||||
def get_event_data(self, event: str) -> dict[str, str | None]:
|
||||
"""Get data to pass along with HA event."""
|
||||
return {
|
||||
@@ -174,18 +248,11 @@ class ConfiguredDoorBird:
|
||||
}
|
||||
|
||||
|
||||
async def async_reset_device_favorites(
|
||||
hass: HomeAssistant, door_station: ConfiguredDoorBird
|
||||
) -> None:
|
||||
async def async_reset_device_favorites(door_station: ConfiguredDoorBird) -> None:
|
||||
"""Handle clearing favorites on device."""
|
||||
await hass.async_add_executor_job(_reset_device_favorites, door_station)
|
||||
|
||||
|
||||
def _reset_device_favorites(door_station: ConfiguredDoorBird) -> None:
|
||||
"""Handle clearing favorites on device."""
|
||||
# Clear webhooks
|
||||
door_bird = door_station.device
|
||||
favorites: dict[str, list[str]] = door_bird.favorites()
|
||||
favorites = await door_bird.favorites()
|
||||
for favorite_type, favorite_ids in favorites.items():
|
||||
for favorite_id in favorite_ids:
|
||||
door_bird.delete_favorite(favorite_type, favorite_id)
|
||||
await door_bird.delete_favorite(favorite_type, favorite_id)
|
||||
await door_station.async_register_events()
|
||||
|
||||
@@ -7,7 +7,8 @@ from homeassistant.components.event import (
|
||||
EventEntity,
|
||||
EventEntityDescription,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -70,14 +71,15 @@ class DoorBirdEventEntity(DoorBirdEntity, EventEntity):
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Subscribe to device events."""
|
||||
self.async_on_remove(
|
||||
self.hass.bus.async_listen(
|
||||
async_dispatcher_connect(
|
||||
self.hass,
|
||||
f"{DOMAIN}_{self._doorbird_event.event}",
|
||||
self._async_handle_event,
|
||||
)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_handle_event(self, event: Event) -> None:
|
||||
def _async_handle_event(self) -> None:
|
||||
"""Handle a device event."""
|
||||
event_types = self.entity_description.event_types
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/doorbird",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["doorbirdpy"],
|
||||
"requirements": ["DoorBirdPy==2.1.0"],
|
||||
"requirements": ["DoorBirdPy==3.0.2"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_axis-video._tcp.local.",
|
||||
|
||||
@@ -23,12 +23,20 @@
|
||||
"data_description": {
|
||||
"host": "The hostname or IP address of your DoorBird device."
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"description": "Re-authenticate DoorBird device {name} at {host}",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"link_local_address": "Link local addresses are not supported",
|
||||
"not_doorbird_device": "This device is not a DoorBird"
|
||||
"not_doorbird_device": "This device is not a DoorBird",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
},
|
||||
"flow_title": "{name} ({host})",
|
||||
"error": {
|
||||
@@ -38,6 +46,14 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"button": {
|
||||
"reset_favorites": {
|
||||
"name": "Reset favorites"
|
||||
},
|
||||
"ir": {
|
||||
"name": "IR"
|
||||
}
|
||||
},
|
||||
"camera": {
|
||||
"live": {
|
||||
"name": "live"
|
||||
|
||||
@@ -7,9 +7,9 @@ from http import HTTPStatus
|
||||
from aiohttp import web
|
||||
|
||||
from homeassistant.components.http import KEY_HASS, HomeAssistantView
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from .const import API_URL, DOMAIN
|
||||
from .device import async_reset_device_favorites
|
||||
from .util import get_door_station_by_token
|
||||
|
||||
|
||||
@@ -38,11 +38,6 @@ class DoorBirdRequestView(HomeAssistantView):
|
||||
else:
|
||||
event_data = {}
|
||||
|
||||
if event == "clear":
|
||||
await async_reset_device_favorites(hass, door_station)
|
||||
message = f"HTTP Favorites cleared for {door_station.slug}"
|
||||
return web.Response(text=message)
|
||||
|
||||
#
|
||||
# This integration uses a multiple different events.
|
||||
# It would be a major breaking change to change this to
|
||||
@@ -51,5 +46,7 @@ class DoorBirdRequestView(HomeAssistantView):
|
||||
# Do not copy this pattern in the future
|
||||
# for any new integrations.
|
||||
#
|
||||
hass.bus.async_fire(f"{DOMAIN}_{event}", event_data)
|
||||
event_type = f"{DOMAIN}_{event}"
|
||||
hass.bus.async_fire(event_type, event_data)
|
||||
async_dispatcher_send(hass, event_type)
|
||||
return web.Response(text="OK")
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"""Support for Dovado router."""
|
||||
|
||||
# mypy: ignore-errors
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
import dovado
|
||||
# import dovado
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"domain": "dovado",
|
||||
"name": "Dovado",
|
||||
"codeowners": [],
|
||||
"disabled": "This integration is disabled because it uses non-open source code to operate.",
|
||||
"documentation": "https://www.home-assistant.io/integrations/dovado",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["dovado==0.4.1"]
|
||||
|
||||
5
homeassistant/components/dovado/ruff.toml
Normal file
5
homeassistant/components/dovado/ruff.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
extend = "../../../pyproject.toml"
|
||||
|
||||
lint.extend-ignore = [
|
||||
"F821"
|
||||
]
|
||||
@@ -16,7 +16,7 @@ from dsmr_parser.clients.rfxtrx_protocol import (
|
||||
create_rfxtrx_dsmr_reader,
|
||||
create_rfxtrx_tcp_dsmr_reader,
|
||||
)
|
||||
from dsmr_parser.objects import DSMRObject
|
||||
from dsmr_parser.objects import DSMRObject, Telegram
|
||||
import serial
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -380,7 +380,7 @@ SENSORS: tuple[DSMRSensorEntityDescription, ...] = (
|
||||
|
||||
|
||||
def create_mbus_entity(
|
||||
mbus: int, mtype: int, telegram: dict[str, DSMRObject]
|
||||
mbus: int, mtype: int, telegram: Telegram
|
||||
) -> DSMRSensorEntityDescription | None:
|
||||
"""Create a new MBUS Entity."""
|
||||
if (
|
||||
@@ -478,7 +478,7 @@ def rename_old_gas_to_mbus(
|
||||
|
||||
|
||||
def create_mbus_entities(
|
||||
hass: HomeAssistant, telegram: dict[str, DSMRObject], entry: ConfigEntry
|
||||
hass: HomeAssistant, telegram: Telegram, entry: ConfigEntry
|
||||
) -> list[DSMREntity]:
|
||||
"""Create MBUS Entities."""
|
||||
entities = []
|
||||
@@ -523,7 +523,7 @@ async def async_setup_entry(
|
||||
add_entities_handler: Callable[..., None] | None
|
||||
|
||||
@callback
|
||||
def init_async_add_entities(telegram: dict[str, DSMRObject]) -> None:
|
||||
def init_async_add_entities(telegram: Telegram) -> None:
|
||||
"""Add the sensor entities after the first telegram was received."""
|
||||
nonlocal add_entities_handler
|
||||
assert add_entities_handler is not None
|
||||
@@ -560,7 +560,7 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
@Throttle(min_time_between_updates)
|
||||
def update_entities_telegram(telegram: dict[str, DSMRObject] | None) -> None:
|
||||
def update_entities_telegram(telegram: Telegram | None) -> None:
|
||||
"""Update entities with latest telegram and trigger state update."""
|
||||
nonlocal initialized
|
||||
# Make all device entities aware of new telegram
|
||||
@@ -709,7 +709,7 @@ class DSMREntity(SensorEntity):
|
||||
self,
|
||||
entity_description: DSMRSensorEntityDescription,
|
||||
entry: ConfigEntry,
|
||||
telegram: dict[str, DSMRObject],
|
||||
telegram: Telegram,
|
||||
device_class: SensorDeviceClass,
|
||||
native_unit_of_measurement: str | None,
|
||||
serial_id: str = "",
|
||||
@@ -720,7 +720,7 @@ class DSMREntity(SensorEntity):
|
||||
self._attr_device_class = device_class
|
||||
self._attr_native_unit_of_measurement = native_unit_of_measurement
|
||||
self._entry = entry
|
||||
self.telegram: dict[str, DSMRObject] | None = telegram
|
||||
self.telegram: Telegram | None = telegram
|
||||
|
||||
device_serial = entry.data[CONF_SERIAL_ID]
|
||||
device_name = DEVICE_NAME_ELECTRICITY
|
||||
@@ -750,7 +750,7 @@ class DSMREntity(SensorEntity):
|
||||
self._attr_unique_id = f"{device_serial}_{entity_description.key}"
|
||||
|
||||
@callback
|
||||
def update_data(self, telegram: dict[str, DSMRObject] | None) -> None:
|
||||
def update_data(self, telegram: Telegram | None) -> None:
|
||||
"""Update data."""
|
||||
self.telegram = telegram
|
||||
if self.hass and (
|
||||
|
||||
@@ -171,12 +171,12 @@
|
||||
},
|
||||
"issues": {
|
||||
"migrate_aux_heat": {
|
||||
"title": "Migration of Ecobee set_aux_heat service",
|
||||
"title": "Migration of Ecobee set_aux_heat action",
|
||||
"fix_flow": {
|
||||
"step": {
|
||||
"confirm": {
|
||||
"description": "The Ecobee `set_aux_heat` service has been migrated. A new `aux_heat_only` switch entity is available for each thermostat that supports a Heat Pump.\n\nUpdate any automations to use the new `aux_heat_only` switch entity. When this is done, fix this issue and restart Home Assistant.",
|
||||
"title": "Disable legacy Ecobee set_aux_heat service"
|
||||
"description": "The Ecobee `set_aux_heat` action has been migrated. A new `aux_heat_only` switch entity is available for each thermostat that supports a Heat Pump.\n\nUpdate any automations to use the new `aux_heat_only` switch entity. When this is done, fix this issue and restart Home Assistant.",
|
||||
"title": "Disable legacy Ecobee set_aux_heat action"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
"fields": {
|
||||
"config_entry": {
|
||||
"name": "Config Entry",
|
||||
"description": "The config entry to use for this service."
|
||||
"description": "The config entry to use for this action."
|
||||
},
|
||||
"incl_vat": {
|
||||
"name": "Including VAT",
|
||||
|
||||
@@ -16,6 +16,8 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_create_clientsession
|
||||
|
||||
from .const import CONF_SOURCE_BOUQUET
|
||||
|
||||
type Enigma2ConfigEntry = ConfigEntry[OpenWebIfDevice]
|
||||
|
||||
PLATFORMS = [Platform.MEDIA_PLAYER]
|
||||
@@ -35,7 +37,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: Enigma2ConfigEntry) -> b
|
||||
hass, verify_ssl=entry.data[CONF_VERIFY_SSL], base_url=base_url
|
||||
)
|
||||
|
||||
entry.runtime_data = OpenWebIfDevice(session)
|
||||
entry.runtime_data = OpenWebIfDevice(
|
||||
session, source_bouquet=entry.options.get(CONF_SOURCE_BOUQUET)
|
||||
)
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["openwebif"],
|
||||
"requirements": ["openwebifpy==4.2.4"]
|
||||
"requirements": ["openwebifpy==4.2.5"]
|
||||
}
|
||||
|
||||
@@ -199,7 +199,8 @@ class Enigma2Device(MediaPlayerEntity):
|
||||
|
||||
async def async_mute_volume(self, mute: bool) -> None:
|
||||
"""Mute or unmute."""
|
||||
await self._device.toggle_mute()
|
||||
if mute != self._device.status.muted:
|
||||
await self._device.toggle_mute()
|
||||
|
||||
async def async_select_source(self, source: str) -> None:
|
||||
"""Select input source."""
|
||||
|
||||
@@ -190,13 +190,13 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]):
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
self._entry_data = entry_data
|
||||
assert entry_data.device_info is not None
|
||||
device_info = entry_data.device_info
|
||||
self._device_info = device_info
|
||||
self._on_entry_data_changed()
|
||||
self._key = entity_info.key
|
||||
self._state_type = state_type
|
||||
self._on_static_info_update(entity_info)
|
||||
assert entry_data.device_info is not None
|
||||
device_info = entry_data.device_info
|
||||
self._device_info = device_info
|
||||
self._attr_device_info = DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, device_info.mac_address)}
|
||||
)
|
||||
@@ -288,6 +288,12 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]):
|
||||
entry_data = self._entry_data
|
||||
self._api_version = entry_data.api_version
|
||||
self._client = entry_data.client
|
||||
if self._device_info.has_deep_sleep:
|
||||
# During deep sleep the ESP will not be connectable (by design)
|
||||
# For these cases, show it as available
|
||||
self._attr_available = entry_data.expected_disconnect
|
||||
else:
|
||||
self._attr_available = entry_data.available
|
||||
|
||||
@callback
|
||||
def _on_device_update(self) -> None:
|
||||
@@ -300,16 +306,6 @@ class EsphomeEntity(Entity, Generic[_InfoT, _StateT]):
|
||||
# through the next entity state packet.
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return if the entity is available."""
|
||||
if self._device_info.has_deep_sleep:
|
||||
# During deep sleep the ESP will not be connectable (by design)
|
||||
# For these cases, show it as available
|
||||
return self._entry_data.expected_disconnect
|
||||
|
||||
return self._entry_data.available
|
||||
|
||||
|
||||
class EsphomeAssistEntity(Entity):
|
||||
"""Define a base entity for Assist Pipeline entities."""
|
||||
|
||||
@@ -122,7 +122,7 @@ def _color_mode_to_ha(mode: int) -> str:
|
||||
return ColorMode.UNKNOWN
|
||||
|
||||
# choose the color mode with the most bits set
|
||||
candidates.sort(key=lambda key: bin(key[1]).count("1"))
|
||||
candidates.sort(key=lambda key: key[1].bit_count())
|
||||
return candidates[-1][0]
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ def _least_complex_color_mode(color_modes: tuple[int, ...]) -> int:
|
||||
# popcount with bin() function because it appears
|
||||
# to be the best way: https://stackoverflow.com/a/9831671
|
||||
color_modes_list = list(color_modes)
|
||||
color_modes_list.sort(key=lambda mode: bin(mode).count("1"))
|
||||
color_modes_list.sort(key=lambda mode: (mode).bit_count())
|
||||
return color_modes_list[0]
|
||||
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ class EsphomeSensor(EsphomeEntity[SensorInfo, SensorState], SensorEntity):
|
||||
state = self._state
|
||||
if state.missing_state or not math.isfinite(state.state):
|
||||
return None
|
||||
if self._attr_device_class == SensorDeviceClass.TIMESTAMP:
|
||||
if self._attr_device_class is SensorDeviceClass.TIMESTAMP:
|
||||
return dt_util.utc_from_timestamp(state.state)
|
||||
return f"{state.state:.{self._static_info.accuracy_decimals}f}"
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"mdns_missing_mac": "Missing MAC address in MDNS properties.",
|
||||
"service_received": "Service received",
|
||||
"service_received": "Action received",
|
||||
"mqtt_missing_mac": "Missing MAC address in MQTT properties.",
|
||||
"mqtt_missing_api": "Missing API port in MQTT properties.",
|
||||
"mqtt_missing_ip": "Missing IP address in MQTT properties."
|
||||
@@ -53,7 +53,7 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"allow_service_calls": "Allow the device to make Home Assistant service calls."
|
||||
"allow_service_calls": "Allow the device to perform Home Assistant actions."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,8 +102,8 @@
|
||||
"description": "The API password for ESPHome is deprecated and the use of an API encryption key is recommended instead.\n\nRemove the API password and add an encryption key to your ESPHome device to resolve this issue."
|
||||
},
|
||||
"service_calls_not_allowed": {
|
||||
"title": "{name} is not permitted to call Home Assistant services",
|
||||
"description": "The ESPHome device attempted to make a Home Assistant service call, but this functionality is not enabled.\n\nIf you trust this device and want to allow it to make Home Assistant service calls, you can enable this functionality in the options flow."
|
||||
"title": "{name} is not permitted to perform Home Assistant actions",
|
||||
"description": "The ESPHome device attempted to perform a Home Assistant action, but this functionality is not enabled.\n\nIf you trust this device and want to allow it to perfom Home Assistant action, you can enable this functionality in the options flow."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"codeowners": ["@dgomes"],
|
||||
"dependencies": ["recorder"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/filter",
|
||||
"integration_type": "helper",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"services": {
|
||||
"ptz": {
|
||||
"name": "PTZ",
|
||||
"description": "Pan/Tilt service for Foscam camera.",
|
||||
"description": "Pan/Tilt action for Foscam camera.",
|
||||
"fields": {
|
||||
"movement": {
|
||||
"name": "Movement",
|
||||
@@ -42,7 +42,7 @@
|
||||
},
|
||||
"ptz_preset": {
|
||||
"name": "PTZ preset",
|
||||
"description": "PTZ Preset service for Foscam camera.",
|
||||
"description": "PTZ Preset action for Foscam camera.",
|
||||
"fields": {
|
||||
"preset_name": {
|
||||
"name": "Preset name",
|
||||
|
||||
@@ -165,10 +165,10 @@
|
||||
},
|
||||
"exceptions": {
|
||||
"config_entry_not_found": {
|
||||
"message": "Failed to call service \"{service}\". Config entry for target not found"
|
||||
"message": "Failed to perform action \"{service}\". Config entry for target not found"
|
||||
},
|
||||
"service_parameter_unknown": { "message": "Service or parameter unknown" },
|
||||
"service_not_supported": { "message": "Service not supported" },
|
||||
"service_parameter_unknown": { "message": "Action or parameter unknown" },
|
||||
"service_not_supported": { "message": "Action not supported" },
|
||||
"error_refresh_hosts_info": {
|
||||
"message": "Error refreshing hosts info"
|
||||
},
|
||||
|
||||
@@ -102,6 +102,7 @@ class FritzBoxCallSensor(SensorEntity):
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_native_value = CallState.IDLE
|
||||
self._attr_device_info = DeviceInfo(
|
||||
configuration_url=self._fritzbox_phonebook.fph.fc.address,
|
||||
identifiers={(DOMAIN, unique_id)},
|
||||
manufacturer=MANUFACTURER,
|
||||
model=self._fritzbox_phonebook.fph.modelname,
|
||||
|
||||
@@ -7,9 +7,9 @@ from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import fan, switch
|
||||
from homeassistant.components.humidifier import HumidifierDeviceClass
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.const import CONF_NAME, PERCENTAGE
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
@@ -45,7 +45,7 @@ OPTIONS_SCHEMA = {
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_HUMIDIFIER): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=SWITCH_DOMAIN)
|
||||
selector.EntitySelectorConfig(domain=[switch.DOMAIN, fan.DOMAIN])
|
||||
),
|
||||
vol.Required(
|
||||
CONF_DRY_TOLERANCE, default=DEFAULT_TOLERANCE
|
||||
|
||||
@@ -7,8 +7,8 @@ from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import fan, switch
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||
from homeassistant.const import CONF_NAME, DEGREE
|
||||
from homeassistant.helpers import selector
|
||||
from homeassistant.helpers.schema_config_entry_flow import (
|
||||
@@ -38,7 +38,7 @@ OPTIONS_SCHEMA = {
|
||||
)
|
||||
),
|
||||
vol.Required(CONF_HEATER): selector.EntitySelector(
|
||||
selector.EntitySelectorConfig(domain=SWITCH_DOMAIN)
|
||||
selector.EntitySelectorConfig(domain=[fan.DOMAIN, switch.DOMAIN])
|
||||
),
|
||||
vol.Required(
|
||||
CONF_COLD_TOLERANCE, default=DEFAULT_TOLERANCE
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"fields": {
|
||||
"agent_user_id": {
|
||||
"name": "Agent user ID",
|
||||
"description": "Only needed for automations. Specific Home Assistant user id (not username, ID in configuration > users > under username) to sync with Google Assistant. Do not need when you call this service through Home Assistant front end or API. Used in automation script or other place where context.user_id is missing."
|
||||
"description": "Only needed for automations. Specific Home Assistant user id (not username, ID in configuration > users > under username) to sync with Google Assistant. Do not need when you use this action through Home Assistant front end or API. Used in automation script or other place where context.user_id is missing."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import operator
|
||||
from types import MappingProxyType
|
||||
from typing import Any
|
||||
|
||||
@@ -74,7 +76,7 @@ def tts_options_schema(
|
||||
): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
mode=SelectSelectorMode.DROPDOWN,
|
||||
options=["", *sum(voices.values(), [])],
|
||||
options=["", *functools.reduce(operator.iadd, voices.values(), [])],
|
||||
)
|
||||
),
|
||||
vol.Optional(
|
||||
|
||||
@@ -41,7 +41,7 @@ SHEET_SERVICE_SCHEMA = vol.All(
|
||||
{
|
||||
vol.Required(DATA_CONFIG_ENTRY): ConfigEntrySelector(),
|
||||
vol.Optional(WORKSHEET): cv.string,
|
||||
vol.Required(DATA): dict,
|
||||
vol.Required(DATA): vol.Any(cv.ensure_list, [dict]),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -108,15 +108,19 @@ async def async_setup_service(hass: HomeAssistant) -> None:
|
||||
raise HomeAssistantError("Failed to write data") from ex
|
||||
|
||||
worksheet = sheet.worksheet(call.data.get(WORKSHEET, sheet.sheet1.title))
|
||||
row_data = {"created": str(datetime.now())} | call.data[DATA]
|
||||
columns: list[str] = next(iter(worksheet.get_values("A1:ZZ1")), [])
|
||||
row = [row_data.get(column, "") for column in columns]
|
||||
for key, value in row_data.items():
|
||||
if key not in columns:
|
||||
columns.append(key)
|
||||
worksheet.update_cell(1, len(columns), key)
|
||||
row.append(value)
|
||||
worksheet.append_row(row, value_input_option=ValueInputOption.user_entered)
|
||||
now = str(datetime.now())
|
||||
rows = []
|
||||
for d in call.data[DATA]:
|
||||
row_data = {"created": now} | d
|
||||
row = [row_data.get(column, "") for column in columns]
|
||||
for key, value in row_data.items():
|
||||
if key not in columns:
|
||||
columns.append(key)
|
||||
worksheet.update_cell(1, len(columns), key)
|
||||
row.append(value)
|
||||
rows.append(row)
|
||||
worksheet.append_rows(rows, value_input_option=ValueInputOption.user_entered)
|
||||
|
||||
async def append_to_sheet(call: ServiceCall) -> None:
|
||||
"""Append new line of data to a Google Sheets document."""
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
},
|
||||
"data": {
|
||||
"name": "Data",
|
||||
"description": "Data to be appended to the worksheet. This puts the values on a new row underneath the matching column (key). Any new key is placed on the top of a new column."
|
||||
"description": "Data to be appended to the worksheet. This puts the values on new rows underneath the matching column (key). Any new key is placed on the top of a new column."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,26 @@
|
||||
"local_name": "B5178*",
|
||||
"connectable": false
|
||||
},
|
||||
{
|
||||
"local_name": "GV5121*",
|
||||
"connectable": false
|
||||
},
|
||||
{
|
||||
"local_name": "GV5122*",
|
||||
"connectable": false
|
||||
},
|
||||
{
|
||||
"local_name": "GV5123*",
|
||||
"connectable": false
|
||||
},
|
||||
{
|
||||
"local_name": "GV5125*",
|
||||
"connectable": false
|
||||
},
|
||||
{
|
||||
"local_name": "GV5126*",
|
||||
"connectable": false
|
||||
},
|
||||
{
|
||||
"manufacturer_id": 1,
|
||||
"service_uuid": "0000ec88-0000-1000-8000-00805f9b34fb",
|
||||
@@ -83,6 +103,10 @@
|
||||
"manufacturer_id": 19506,
|
||||
"service_uuid": "00001801-0000-1000-8000-00805f9b34fb",
|
||||
"connectable": false
|
||||
},
|
||||
{
|
||||
"manufacturer_id": 61320,
|
||||
"connectable": false
|
||||
}
|
||||
],
|
||||
"codeowners": ["@bdraco", "@PierreAronnax"],
|
||||
@@ -90,5 +114,5 @@
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/govee_ble",
|
||||
"iot_class": "local_push",
|
||||
"requirements": ["govee-ble==0.31.3"]
|
||||
"requirements": ["govee-ble==0.33.0"]
|
||||
}
|
||||
|
||||
131
homeassistant/components/group/button.py
Normal file
131
homeassistant/components/group/button.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""Platform allowing several button entities to be grouped into one single button."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.button import (
|
||||
DOMAIN,
|
||||
PLATFORM_SCHEMA as BUTTON_PLATFORM_SCHEMA,
|
||||
SERVICE_PRESS,
|
||||
ButtonEntity,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
CONF_ENTITIES,
|
||||
CONF_NAME,
|
||||
CONF_UNIQUE_ID,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
|
||||
from .entity import GroupEntity
|
||||
|
||||
DEFAULT_NAME = "Button group"
|
||||
|
||||
# No limit on parallel updates to enable a group calling another group
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
PLATFORM_SCHEMA = BUTTON_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_ENTITIES): cv.entities_domain(DOMAIN),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_UNIQUE_ID): cv.string,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
_: HomeAssistant,
|
||||
config: ConfigType,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
__: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the button group platform."""
|
||||
async_add_entities(
|
||||
[
|
||||
ButtonGroup(
|
||||
config.get(CONF_UNIQUE_ID),
|
||||
config[CONF_NAME],
|
||||
config[CONF_ENTITIES],
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Initialize button group config entry."""
|
||||
registry = er.async_get(hass)
|
||||
entities = er.async_validate_entity_ids(
|
||||
registry, config_entry.options[CONF_ENTITIES]
|
||||
)
|
||||
async_add_entities(
|
||||
[
|
||||
ButtonGroup(
|
||||
config_entry.entry_id,
|
||||
config_entry.title,
|
||||
entities,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_create_preview_button(
|
||||
hass: HomeAssistant, name: str, validated_config: dict[str, Any]
|
||||
) -> ButtonGroup:
|
||||
"""Create a preview button."""
|
||||
return ButtonGroup(
|
||||
None,
|
||||
name,
|
||||
validated_config[CONF_ENTITIES],
|
||||
)
|
||||
|
||||
|
||||
class ButtonGroup(GroupEntity, ButtonEntity):
|
||||
"""Representation of an button group."""
|
||||
|
||||
_attr_available = False
|
||||
_attr_should_poll = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
unique_id: str | None,
|
||||
name: str,
|
||||
entity_ids: list[str],
|
||||
) -> None:
|
||||
"""Initialize a button group."""
|
||||
self._entity_ids = entity_ids
|
||||
self._attr_name = name
|
||||
self._attr_extra_state_attributes = {ATTR_ENTITY_ID: entity_ids}
|
||||
self._attr_unique_id = unique_id
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Forward the press to all buttons in the group."""
|
||||
await self.hass.services.async_call(
|
||||
DOMAIN,
|
||||
SERVICE_PRESS,
|
||||
{ATTR_ENTITY_ID: self._entity_ids},
|
||||
blocking=True,
|
||||
context=self._context,
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_update_group_state(self) -> None:
|
||||
"""Query all members and determine the button group state."""
|
||||
# Set group as unavailable if all members are unavailable or missing
|
||||
self._attr_available = any(
|
||||
state.state != STATE_UNAVAILABLE
|
||||
for entity_id in self._entity_ids
|
||||
if (state := self.hass.states.get(entity_id)) is not None
|
||||
)
|
||||
@@ -23,6 +23,7 @@ from homeassistant.helpers.schema_config_entry_flow import (
|
||||
)
|
||||
|
||||
from .binary_sensor import CONF_ALL, async_create_preview_binary_sensor
|
||||
from .button import async_create_preview_button
|
||||
from .const import CONF_HIDE_MEMBERS, CONF_IGNORE_NON_NUMERIC, DOMAIN
|
||||
from .cover import async_create_preview_cover
|
||||
from .entity import GroupEntity
|
||||
@@ -146,6 +147,7 @@ async def light_switch_options_schema(
|
||||
|
||||
GROUP_TYPES = [
|
||||
"binary_sensor",
|
||||
"button",
|
||||
"cover",
|
||||
"event",
|
||||
"fan",
|
||||
@@ -185,6 +187,11 @@ CONFIG_FLOW = {
|
||||
preview="group",
|
||||
validate_user_input=set_group_type("binary_sensor"),
|
||||
),
|
||||
"button": SchemaFlowFormStep(
|
||||
basic_group_config_schema("button"),
|
||||
preview="group",
|
||||
validate_user_input=set_group_type("button"),
|
||||
),
|
||||
"cover": SchemaFlowFormStep(
|
||||
basic_group_config_schema("cover"),
|
||||
preview="group",
|
||||
@@ -234,6 +241,10 @@ OPTIONS_FLOW = {
|
||||
binary_sensor_options_schema,
|
||||
preview="group",
|
||||
),
|
||||
"button": SchemaFlowFormStep(
|
||||
partial(basic_group_options_schema, "button"),
|
||||
preview="group",
|
||||
),
|
||||
"cover": SchemaFlowFormStep(
|
||||
partial(basic_group_options_schema, "cover"),
|
||||
preview="group",
|
||||
@@ -275,6 +286,7 @@ CREATE_PREVIEW_ENTITY: dict[
|
||||
Callable[[HomeAssistant, str, dict[str, Any]], GroupEntity | MediaPlayerGroup],
|
||||
] = {
|
||||
"binary_sensor": async_create_preview_binary_sensor,
|
||||
"button": async_create_preview_button,
|
||||
"cover": async_create_preview_cover,
|
||||
"event": async_create_preview_event,
|
||||
"fan": async_create_preview_fan,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"description": "Groups allow you to create a new entity that represents multiple entities of the same type.",
|
||||
"menu_options": {
|
||||
"binary_sensor": "Binary sensor group",
|
||||
"button": "Button group",
|
||||
"cover": "Cover group",
|
||||
"event": "Event group",
|
||||
"fan": "Fan group",
|
||||
@@ -27,6 +28,14 @@
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"title": "[%key:component::group::config::step::user::title%]",
|
||||
"data": {
|
||||
"entities": "[%key:component::group::config::step::binary_sensor::data::entities%]",
|
||||
"hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]",
|
||||
"name": "[%key:common::config_flow::data::name%]"
|
||||
}
|
||||
},
|
||||
"cover": {
|
||||
"title": "[%key:component::group::config::step::user::title%]",
|
||||
"data": {
|
||||
@@ -109,6 +118,12 @@
|
||||
"hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]"
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"data": {
|
||||
"entities": "[%key:component::group::config::step::binary_sensor::data::entities%]",
|
||||
"hide_members": "[%key:component::group::config::step::binary_sensor::data::hide_members%]"
|
||||
}
|
||||
},
|
||||
"cover": {
|
||||
"data": {
|
||||
"entities": "[%key:component::group::config::step::binary_sensor::data::entities%]",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"user": {
|
||||
"data": {
|
||||
"url": "[%key:common::config_flow::data::url%]",
|
||||
"name": "Override for Habitica’s username. Will be used for service calls",
|
||||
"name": "Override for Habitica’s username. Will be used for actions",
|
||||
"api_user": "Habitica’s API user ID",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
|
||||
@@ -127,6 +127,7 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
|
||||
|
||||
if (
|
||||
self.entity_description.key is HabiticaTodoList.TODOS
|
||||
and item.due is not None
|
||||
): # Only todos support a due date.
|
||||
date = item.due.isoformat()
|
||||
else:
|
||||
@@ -149,14 +150,14 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
|
||||
# Score up or down if item status changed
|
||||
if (
|
||||
current_item.status is TodoItemStatus.NEEDS_ACTION
|
||||
and item.status is TodoItemStatus.COMPLETED
|
||||
and item.status == TodoItemStatus.COMPLETED
|
||||
):
|
||||
score_result = (
|
||||
await self.coordinator.api.tasks[item.uid].score["up"].post()
|
||||
)
|
||||
elif (
|
||||
current_item.status is TodoItemStatus.COMPLETED
|
||||
and item.status is TodoItemStatus.NEEDS_ACTION
|
||||
and item.status == TodoItemStatus.NEEDS_ACTION
|
||||
):
|
||||
score_result = (
|
||||
await self.coordinator.api.tasks[item.uid].score["down"].post()
|
||||
|
||||
@@ -289,7 +289,7 @@
|
||||
},
|
||||
"addon_update": {
|
||||
"name": "Update add-on.",
|
||||
"description": "Updates an add-on. This service should be used with caution since add-on updates can contain breaking changes. It is highly recommended that you review release notes/change logs before updating an add-on.",
|
||||
"description": "Updates an add-on. This action should be used with caution since add-on updates can contain breaking changes. It is highly recommended that you review release notes/change logs before updating an add-on.",
|
||||
"fields": {
|
||||
"addon": {
|
||||
"name": "[%key:component::hassio::services::addon_start::fields::addon::name%]",
|
||||
|
||||
@@ -7,6 +7,9 @@ from datetime import timedelta
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_ENTITY_ID, CONF_STATE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device import (
|
||||
async_remove_stale_devices_links_keep_entity_device,
|
||||
)
|
||||
from homeassistant.helpers.template import Template
|
||||
|
||||
from .const import CONF_DURATION, CONF_END, CONF_START, PLATFORMS
|
||||
@@ -42,6 +45,12 @@ async def async_setup_entry(
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
async_remove_stale_devices_links_keep_entity_device(
|
||||
hass,
|
||||
entry.entry_id,
|
||||
entry.options[CONF_ENTITY_ID],
|
||||
)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
entry.async_on_unload(entry.add_update_listener(update_listener))
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.device import async_device_info_to_link_from_entity
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.reload import async_setup_reload_service
|
||||
from homeassistant.helpers.template import Template
|
||||
@@ -111,7 +112,9 @@ async def async_setup_platform(
|
||||
await coordinator.async_refresh()
|
||||
if not coordinator.last_update_success:
|
||||
raise PlatformNotReady from coordinator.last_exception
|
||||
async_add_entities([HistoryStatsSensor(coordinator, sensor_type, name, unique_id)])
|
||||
async_add_entities(
|
||||
[HistoryStatsSensor(hass, coordinator, sensor_type, name, unique_id, entity_id)]
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -123,8 +126,13 @@ async def async_setup_entry(
|
||||
|
||||
sensor_type: str = entry.options[CONF_TYPE]
|
||||
coordinator = entry.runtime_data
|
||||
entity_id: str = entry.options[CONF_ENTITY_ID]
|
||||
async_add_entities(
|
||||
[HistoryStatsSensor(coordinator, sensor_type, entry.title, entry.entry_id)]
|
||||
[
|
||||
HistoryStatsSensor(
|
||||
hass, coordinator, sensor_type, entry.title, entry.entry_id, entity_id
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
@@ -167,16 +175,22 @@ class HistoryStatsSensor(HistoryStatsSensorBase):
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
coordinator: HistoryStatsUpdateCoordinator,
|
||||
sensor_type: str,
|
||||
name: str,
|
||||
unique_id: str | None,
|
||||
source_entity_id: str,
|
||||
) -> None:
|
||||
"""Initialize the HistoryStats sensor."""
|
||||
super().__init__(coordinator, name)
|
||||
self._attr_native_unit_of_measurement = UNITS[sensor_type]
|
||||
self._type = sensor_type
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_device_info = async_device_info_to_link_from_entity(
|
||||
hass,
|
||||
source_entity_id,
|
||||
)
|
||||
self._process_update()
|
||||
if self._type == CONF_TYPE_TIME:
|
||||
self._attr_device_class = SensorDeviceClass.DURATION
|
||||
|
||||
@@ -133,15 +133,15 @@
|
||||
},
|
||||
"toggle": {
|
||||
"name": "Generic toggle",
|
||||
"description": "Generic service to toggle devices on/off under any domain."
|
||||
"description": "Generic action to toggle devices on/off under any domain."
|
||||
},
|
||||
"turn_on": {
|
||||
"name": "Generic turn on",
|
||||
"description": "Generic service to turn devices on under any domain."
|
||||
"description": "Generic action to turn devices on under any domain."
|
||||
},
|
||||
"turn_off": {
|
||||
"name": "Generic turn off",
|
||||
"description": "Generic service to turn devices off under any domain."
|
||||
"description": "Generic action to turn devices off under any domain."
|
||||
},
|
||||
"update_entity": {
|
||||
"name": "Update entity",
|
||||
@@ -205,19 +205,19 @@
|
||||
"message": "Unknown error when validating config for {domain} from integration {p_name} - {error}."
|
||||
},
|
||||
"service_not_found": {
|
||||
"message": "Service {domain}.{service} not found."
|
||||
"message": "Action {domain}.{service} not found."
|
||||
},
|
||||
"service_does_not_support_response": {
|
||||
"message": "A service which does not return responses can't be called with {return_response}."
|
||||
"message": "An action which does not return responses can't be called with {return_response}."
|
||||
},
|
||||
"service_lacks_response_request": {
|
||||
"message": "The service call requires responses and must be called with {return_response}."
|
||||
"message": "The action requires responses and must be called with {return_response}."
|
||||
},
|
||||
"service_reponse_invalid": {
|
||||
"message": "Failed to process the returned service response data, expected a dictionary, but got {response_data_type}."
|
||||
"message": "Failed to process the returned action response data, expected a dictionary, but got {response_data_type}."
|
||||
},
|
||||
"service_should_be_blocking": {
|
||||
"message": "A non blocking service call with argument {non_blocking_argument} can't be used together with argument {return_response}."
|
||||
"message": "A non blocking action call with argument {non_blocking_argument} can't be used together with argument {return_response}."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
},
|
||||
"unpair": {
|
||||
"name": "Unpair an accessory or bridge",
|
||||
"description": "Forcefully removes all pairings from an accessory to allow re-pairing. Use this service if the accessory is no longer responsive, and you want to avoid deleting and re-adding the entry. Room locations, and accessory preferences will be lost."
|
||||
"description": "Forcefully removes all pairings from an accessory to allow re-pairing. Use this action if the accessory is no longer responsive, and you want to avoid deleting and re-adding the entry. Room locations, and accessory preferences will be lost."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,8 +233,7 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc]
|
||||
self._char_motion_detected = serv_motion.configure_char(
|
||||
CHAR_MOTION_DETECTED, value=False
|
||||
)
|
||||
if not self.motion_is_event:
|
||||
self._async_update_motion_state(state)
|
||||
self._async_update_motion_state(None, state)
|
||||
|
||||
self._char_doorbell_detected = None
|
||||
self._char_doorbell_detected_switch = None
|
||||
@@ -264,9 +263,7 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc]
|
||||
)
|
||||
serv_speaker = self.add_preload_service(SERV_SPEAKER)
|
||||
serv_speaker.configure_char(CHAR_MUTE, value=0)
|
||||
|
||||
if not self.doorbell_is_event:
|
||||
self._async_update_doorbell_state(state)
|
||||
self._async_update_doorbell_state(None, state)
|
||||
|
||||
@pyhap_callback # type: ignore[misc]
|
||||
@callback
|
||||
@@ -304,20 +301,25 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc]
|
||||
self, event: Event[EventStateChangedData]
|
||||
) -> None:
|
||||
"""Handle state change event listener callback."""
|
||||
if not state_changed_event_is_same_state(event):
|
||||
self._async_update_motion_state(event.data["new_state"])
|
||||
if not state_changed_event_is_same_state(event) and (
|
||||
new_state := event.data["new_state"]
|
||||
):
|
||||
self._async_update_motion_state(event.data["old_state"], new_state)
|
||||
|
||||
@callback
|
||||
def _async_update_motion_state(self, new_state: State | None) -> None:
|
||||
def _async_update_motion_state(
|
||||
self, old_state: State | None, new_state: State
|
||||
) -> None:
|
||||
"""Handle link motion sensor state change to update HomeKit value."""
|
||||
if not new_state:
|
||||
return
|
||||
|
||||
state = new_state.state
|
||||
char = self._char_motion_detected
|
||||
assert char is not None
|
||||
if self.motion_is_event:
|
||||
if state in (STATE_UNKNOWN, STATE_UNAVAILABLE):
|
||||
if (
|
||||
old_state is None
|
||||
or old_state.state == STATE_UNAVAILABLE
|
||||
or state in (STATE_UNKNOWN, STATE_UNAVAILABLE)
|
||||
):
|
||||
return
|
||||
_LOGGER.debug(
|
||||
"%s: Set linked motion %s sensor to True/False",
|
||||
@@ -348,16 +350,21 @@ class Camera(HomeAccessory, PyhapCamera): # type: ignore[misc]
|
||||
if not state_changed_event_is_same_state(event) and (
|
||||
new_state := event.data["new_state"]
|
||||
):
|
||||
self._async_update_doorbell_state(new_state)
|
||||
self._async_update_doorbell_state(event.data["old_state"], new_state)
|
||||
|
||||
@callback
|
||||
def _async_update_doorbell_state(self, new_state: State) -> None:
|
||||
def _async_update_doorbell_state(
|
||||
self, old_state: State | None, new_state: State
|
||||
) -> None:
|
||||
"""Handle link doorbell sensor state change to update HomeKit value."""
|
||||
assert self._char_doorbell_detected
|
||||
assert self._char_doorbell_detected_switch
|
||||
state = new_state.state
|
||||
if state == STATE_ON or (
|
||||
self.doorbell_is_event and state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
|
||||
self.doorbell_is_event
|
||||
and old_state is not None
|
||||
and old_state.state != STATE_UNAVAILABLE
|
||||
and state not in (STATE_UNKNOWN, STATE_UNAVAILABLE)
|
||||
):
|
||||
self._char_doorbell_detected.set_value(DOORBELL_SINGLE_PRESS)
|
||||
self._char_doorbell_detected_switch.set_value(DOORBELL_SINGLE_PRESS)
|
||||
|
||||
@@ -361,7 +361,7 @@
|
||||
},
|
||||
"suspend_integration": {
|
||||
"name": "Suspend integration",
|
||||
"description": "Suspends integration. Suspending logs the integration out from the router, and stops accessing it. Useful e.g. if accessing the router web interface from another source such as a web browser is temporarily required. Invoke the resume_integration service to resume.\n.",
|
||||
"description": "Suspends integration. Suspending logs the integration out from the router, and stops accessing it. Useful e.g. if accessing the router web interface from another source such as a web browser is temporarily required. Invoke the resume_integration action to resume.\n.",
|
||||
"fields": {
|
||||
"url": {
|
||||
"name": "[%key:common::config_flow::data::url%]",
|
||||
|
||||
@@ -9,7 +9,6 @@ from aiopvapi.rooms import Rooms
|
||||
from aiopvapi.scenes import Scenes
|
||||
from aiopvapi.shades import Shades
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_VERSION, CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
@@ -18,7 +17,7 @@ import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from .const import DOMAIN, HUB_EXCEPTIONS
|
||||
from .coordinator import PowerviewShadeUpdateCoordinator
|
||||
from .model import PowerviewDeviceInfo, PowerviewEntryData
|
||||
from .model import PowerviewConfigEntry, PowerviewDeviceInfo, PowerviewEntryData
|
||||
from .shade_data import PowerviewShadeData
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
@@ -36,7 +35,7 @@ PLATFORMS = [
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: PowerviewConfigEntry) -> bool:
|
||||
"""Set up Hunter Douglas PowerView from a config entry."""
|
||||
|
||||
config = entry.data
|
||||
@@ -100,7 +99,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# populate raw shade data into the coordinator for diagnostics
|
||||
coordinator.data.store_group_data(shade_data)
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = PowerviewEntryData(
|
||||
entry.runtime_data = PowerviewEntryData(
|
||||
api=pv_request,
|
||||
room_data=room_data.processed,
|
||||
scene_data=scene_data.processed,
|
||||
@@ -126,8 +125,6 @@ async def async_get_device_info(hub: Hub) -> PowerviewDeviceInfo:
|
||||
)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: PowerviewConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
@@ -20,15 +20,13 @@ from homeassistant.components.button import (
|
||||
ButtonEntity,
|
||||
ButtonEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import PowerviewShadeUpdateCoordinator
|
||||
from .entity import ShadeEntity
|
||||
from .model import PowerviewDeviceInfo, PowerviewEntryData
|
||||
from .model import PowerviewConfigEntry, PowerviewDeviceInfo
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -75,13 +73,11 @@ BUTTONS_SHADE: Final = [
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: PowerviewConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the hunter douglas advanced feature buttons."""
|
||||
|
||||
pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
pv_entry = entry.runtime_data
|
||||
entities: list[ButtonEntity] = []
|
||||
for shade in pv_entry.shade_data.values():
|
||||
room_name = getattr(pv_entry.room_data.get(shade.room_id), ATTR_NAME, "")
|
||||
|
||||
@@ -25,15 +25,14 @@ from homeassistant.components.cover import (
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
from .const import DOMAIN, STATE_ATTRIBUTE_ROOM_NAME
|
||||
from .const import STATE_ATTRIBUTE_ROOM_NAME
|
||||
from .coordinator import PowerviewShadeUpdateCoordinator
|
||||
from .entity import ShadeEntity
|
||||
from .model import PowerviewDeviceInfo, PowerviewEntryData
|
||||
from .model import PowerviewConfigEntry, PowerviewDeviceInfo
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -49,12 +48,13 @@ SCAN_INTERVAL = timedelta(minutes=10)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
||||
hass: HomeAssistant,
|
||||
entry: PowerviewConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the hunter douglas shades."""
|
||||
|
||||
pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id]
|
||||
coordinator: PowerviewShadeUpdateCoordinator = pv_entry.coordinator
|
||||
pv_entry = entry.runtime_data
|
||||
coordinator = pv_entry.coordinator
|
||||
|
||||
async def _async_initial_refresh() -> None:
|
||||
"""Force position refresh shortly after adding.
|
||||
|
||||
@@ -3,20 +3,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import attr
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_CONFIGURATION_URL, CONF_HOST
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
from .const import DOMAIN, REDACT_HUB_ADDRESS, REDACT_MAC_ADDRESS, REDACT_SERIAL_NUMBER
|
||||
from .model import PowerviewEntryData
|
||||
from .const import REDACT_HUB_ADDRESS, REDACT_MAC_ADDRESS, REDACT_SERIAL_NUMBER
|
||||
from .model import PowerviewConfigEntry
|
||||
|
||||
REDACT_CONFIG = {
|
||||
CONF_HOST,
|
||||
@@ -26,11 +24,9 @@ REDACT_CONFIG = {
|
||||
ATTR_CONFIGURATION_URL,
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
hass: HomeAssistant, entry: PowerviewConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
data = _async_get_diagnostics(hass, entry)
|
||||
@@ -47,7 +43,7 @@ async def async_get_config_entry_diagnostics(
|
||||
|
||||
|
||||
async def async_get_device_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry, device: DeviceEntry
|
||||
hass: HomeAssistant, entry: PowerviewConfigEntry, device: DeviceEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a device entry."""
|
||||
data = _async_get_diagnostics(hass, entry)
|
||||
@@ -65,10 +61,10 @@ async def async_get_device_diagnostics(
|
||||
@callback
|
||||
def _async_get_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
entry: PowerviewConfigEntry,
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
pv_entry: PowerviewEntryData = hass.data[DOMAIN][entry.entry_id]
|
||||
pv_entry = entry.runtime_data
|
||||
shade_data = pv_entry.coordinator.data.get_all_raw_data()
|
||||
hub_info = async_redact_data(asdict(pv_entry.device_info), REDACT_CONFIG)
|
||||
return {"hub_info": hub_info, "shade_data": shade_data}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user