Merge branch 'home-assistant:dev' into flussButton

This commit is contained in:
NjeruFluss
2024-07-16 09:03:10 +02:00
committed by GitHub
356 changed files with 11031 additions and 1789 deletions

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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"

View File

@@ -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 }}

View File

@@ -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

View File

@@ -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:

View File

@@ -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.*

View File

@@ -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

View File

@@ -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):

View File

@@ -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):

View File

@@ -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"]
}

View File

@@ -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):

View File

@@ -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):

View File

@@ -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)])

View File

@@ -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"]
}

View File

@@ -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))

View File

@@ -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"]
}

View File

@@ -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
}

View File

@@ -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": {

View File

@@ -8,5 +8,5 @@
"integration_type": "device",
"iot_class": "cloud_polling",
"loggers": ["aioaquacell"],
"requirements": ["aioaquacell==0.1.8"]
"requirements": ["aioaquacell==0.2.0"]
}

View File

@@ -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"]
}

View 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)

View 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,
)

View 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)

View 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),
)

View 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
],
}

View 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"]
}

View 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]
)

View 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"
}
}
}
}

View File

@@ -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."
}
}
}

View File

@@ -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"]
}

View File

@@ -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)

View File

@@ -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"
}

View File

@@ -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]:

View File

@@ -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 (

View File

@@ -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"],

View File

@@ -0,0 +1,5 @@
extend = "../../../pyproject.toml"
lint.extend-ignore = [
"F821"
]

View File

@@ -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,

View File

@@ -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"

View File

@@ -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())

View 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()

View File

@@ -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%]"
}
}
}
}
}

View 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()

View File

@@ -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."
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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"],

View File

@@ -0,0 +1,5 @@
extend = "../../../pyproject.toml"
lint.extend-ignore = [
"F821"
]

View File

@@ -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"]
}

View File

@@ -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)

View File

@@ -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,
(

View File

@@ -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)

View File

@@ -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

View File

@@ -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, [])

View File

@@ -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

View File

@@ -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()

View File

@@ -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:

View File

@@ -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.",

View File

@@ -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"

View File

@@ -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")

View File

@@ -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 (

View File

@@ -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"]

View File

@@ -0,0 +1,5 @@
extend = "../../../pyproject.toml"
lint.extend-ignore = [
"F821"
]

View File

@@ -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 (

View File

@@ -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"
}
}
}

View File

@@ -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",

View File

@@ -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

View File

@@ -7,5 +7,5 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["openwebif"],
"requirements": ["openwebifpy==4.2.4"]
"requirements": ["openwebifpy==4.2.5"]
}

View File

@@ -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."""

View File

@@ -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."""

View File

@@ -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]

View File

@@ -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}"

View File

@@ -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."
}
}
}

View File

@@ -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"
}

View File

@@ -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",

View File

@@ -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"
},

View File

@@ -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,

View File

@@ -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

View File

@@ -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

View File

@@ -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."
}
}
}

View File

@@ -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(

View File

@@ -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."""

View File

@@ -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."
}
}
}

View File

@@ -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"]
}

View 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
)

View File

@@ -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,

View File

@@ -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%]",

View File

@@ -11,7 +11,7 @@
"user": {
"data": {
"url": "[%key:common::config_flow::data::url%]",
"name": "Override for Habiticas username. Will be used for service calls",
"name": "Override for Habiticas username. Will be used for actions",
"api_user": "Habiticas API user ID",
"api_key": "[%key:common::config_flow::data::api_key%]"
},

View File

@@ -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()

View File

@@ -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%]",

View File

@@ -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))

View File

@@ -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

View File

@@ -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}."
}
}
}

View File

@@ -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."
}
}
}

View File

@@ -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)

View File

@@ -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%]",

View File

@@ -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)

View File

@@ -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, "")

View File

@@ -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.

View File

@@ -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