Merge branch 'dev' into heatpump

This commit is contained in:
Christopher Fenner
2024-02-16 09:02:38 +01:00
committed by GitHub
1357 changed files with 29453 additions and 8638 deletions

View File

@@ -73,6 +73,10 @@ omit =
homeassistant/components/apple_tv/browse_media.py
homeassistant/components/apple_tv/media_player.py
homeassistant/components/apple_tv/remote.py
homeassistant/components/aprilaire/__init__.py
homeassistant/components/aprilaire/climate.py
homeassistant/components/aprilaire/coordinator.py
homeassistant/components/aprilaire/entity.py
homeassistant/components/aqualogic/*
homeassistant/components/aquostv/media_player.py
homeassistant/components/arcam_fmj/__init__.py
@@ -484,6 +488,7 @@ omit =
homeassistant/components/gpsd/sensor.py
homeassistant/components/greenwave/light.py
homeassistant/components/growatt_server/__init__.py
homeassistant/components/growatt_server/const.py
homeassistant/components/growatt_server/sensor.py
homeassistant/components/growatt_server/sensor_types/*
homeassistant/components/gstreamer/media_player.py
@@ -872,6 +877,7 @@ omit =
homeassistant/components/notion/__init__.py
homeassistant/components/notion/binary_sensor.py
homeassistant/components/notion/sensor.py
homeassistant/components/notion/util.py
homeassistant/components/nsw_fuel_station/sensor.py
homeassistant/components/nuki/__init__.py
homeassistant/components/nuki/binary_sensor.py
@@ -1533,6 +1539,7 @@ omit =
homeassistant/components/vicare/entity.py
homeassistant/components/vicare/number.py
homeassistant/components/vicare/sensor.py
homeassistant/components/vicare/types.py
homeassistant/components/vicare/utils.py
homeassistant/components/vicare/water_heater.py
homeassistant/components/vilfo/__init__.py
@@ -1691,8 +1698,10 @@ omit =
homeassistant/components/myuplink/__init__.py
homeassistant/components/myuplink/api.py
homeassistant/components/myuplink/application_credentials.py
homeassistant/components/myuplink/binary_sensor.py
homeassistant/components/myuplink/coordinator.py
homeassistant/components/myuplink/entity.py
homeassistant/components/myuplink/helpers.py
homeassistant/components/myuplink/sensor.py

View File

@@ -1079,7 +1079,7 @@ jobs:
uses: actions/download-artifact@v3
- name: Upload coverage to Codecov (full coverage)
if: needs.info.outputs.test_full_suite == 'true'
uses: Wandalen/wretry.action@v1.3.0
uses: Wandalen/wretry.action@v1.4.4
with:
action: codecov/codecov-action@v3.1.3
with: |
@@ -1090,7 +1090,7 @@ jobs:
attempt_delay: 30000
- name: Upload coverage to Codecov (partial coverage)
if: needs.info.outputs.test_full_suite == 'false'
uses: Wandalen/wretry.action@v1.3.0
uses: Wandalen/wretry.action@v1.4.4
with:
action: codecov/codecov-action@v3.1.3
with: |

View File

@@ -29,11 +29,11 @@ jobs:
uses: actions/checkout@v4.1.1
- name: Initialize CodeQL
uses: github/codeql-action/init@v3.23.2
uses: github/codeql-action/init@v3.24.1
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3.23.2
uses: github/codeql-action/analyze@v3.24.1
with:
category: "/language:python"

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.15
rev: v0.2.1
hooks:
- id: ruff
args:

View File

@@ -80,6 +80,7 @@ homeassistant.components.anthemav.*
homeassistant.components.apache_kafka.*
homeassistant.components.apcupsd.*
homeassistant.components.api.*
homeassistant.components.apple_tv.*
homeassistant.components.apprise.*
homeassistant.components.aprs.*
homeassistant.components.aqualogic.*

View File

@@ -104,6 +104,8 @@ build.json @home-assistant/supervisor
/tests/components/application_credentials/ @home-assistant/core
/homeassistant/components/apprise/ @caronc
/tests/components/apprise/ @caronc
/homeassistant/components/aprilaire/ @chamberlain2007
/tests/components/aprilaire/ @chamberlain2007
/homeassistant/components/aprs/ @PhilRW
/tests/components/aprs/ @PhilRW
/homeassistant/components/aranet/ @aschmitz @thecode
@@ -584,6 +586,8 @@ build.json @home-assistant/supervisor
/tests/components/humidifier/ @home-assistant/core @Shulyaka
/homeassistant/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
/tests/components/hunterdouglas_powerview/ @bdraco @kingy444 @trullock
/homeassistant/components/husqvarna_automower/ @Thomas55555
/tests/components/husqvarna_automower/ @Thomas55555
/homeassistant/components/huum/ @frwickst
/tests/components/huum/ @frwickst
/homeassistant/components/hvv_departures/ @vigonotion
@@ -786,8 +790,6 @@ build.json @home-assistant/supervisor
/homeassistant/components/media_source/ @hunterjm
/tests/components/media_source/ @hunterjm
/homeassistant/components/mediaroom/ @dgomes
/homeassistant/components/melcloud/ @vilppuvuorinen
/tests/components/melcloud/ @vilppuvuorinen
/homeassistant/components/melissa/ @kennedyshead
/tests/components/melissa/ @kennedyshead
/homeassistant/components/melnor/ @vanstinator
@@ -1460,7 +1462,8 @@ build.json @home-assistant/supervisor
/tests/components/valve/ @home-assistant/core
/homeassistant/components/velbus/ @Cereal2nd @brefra
/tests/components/velbus/ @Cereal2nd @brefra
/homeassistant/components/velux/ @Julius2342
/homeassistant/components/velux/ @Julius2342 @DeerMaximum
/tests/components/velux/ @Julius2342 @DeerMaximum
/homeassistant/components/venstar/ @garbled1 @jhollowe
/tests/components/venstar/ @garbled1 @jhollowe
/homeassistant/components/versasense/ @imstevenxyz

View File

@@ -1,10 +1,10 @@
image: ghcr.io/home-assistant/{arch}-homeassistant
build_from:
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.02.0
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.02.0
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.02.0
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.02.0
i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.02.0
aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2024.02.1
armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2024.02.1
armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2024.02.1
amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2024.02.1
i386: ghcr.io/home-assistant/i386-homeassistant-base:2024.02.1
codenotary:
signer: notary@home-assistant.io
base_image: notary@home-assistant.io

View File

@@ -35,6 +35,7 @@ from .helpers import (
recorder,
restore_state,
template,
translation,
)
from .helpers.dispatcher import async_dispatcher_send
from .helpers.typing import ConfigType
@@ -217,7 +218,7 @@ async def async_setup_hass(
)
# Ask integrations to shut down. It's messy but we can't
# do a clean stop without knowing what is broken
with contextlib.suppress(asyncio.TimeoutError):
with contextlib.suppress(TimeoutError):
async with hass.timeout.async_timeout(10):
await hass.async_stop()
@@ -291,6 +292,7 @@ async def async_load_base_functionality(hass: core.HomeAssistant) -> None:
platform.uname().processor # pylint: disable=expression-not-assigned
# Load the registries and cache the result of platform.uname().processor
translation.async_setup(hass)
entity.async_setup(hass)
template.async_setup(hass)
await asyncio.gather(
@@ -738,7 +740,7 @@ async def _async_set_up_integrations(
STAGE_1_TIMEOUT, cool_down=COOLDOWN_TIME
):
await async_setup_multi_components(hass, stage_1_domains, config)
except asyncio.TimeoutError:
except TimeoutError:
_LOGGER.warning("Setup timed out for stage 1 - moving forward")
# Add after dependencies when setting up stage 2 domains
@@ -751,7 +753,7 @@ async def _async_set_up_integrations(
STAGE_2_TIMEOUT, cool_down=COOLDOWN_TIME
):
await async_setup_multi_components(hass, stage_2_domains, config)
except asyncio.TimeoutError:
except TimeoutError:
_LOGGER.warning("Setup timed out for stage 2 - moving forward")
# Wrap up startup
@@ -759,7 +761,7 @@ async def _async_set_up_integrations(
try:
async with hass.timeout.async_timeout(WRAP_UP_TIMEOUT, cool_down=COOLDOWN_TIME):
await hass.async_block_till_done()
except asyncio.TimeoutError:
except TimeoutError:
_LOGGER.warning("Setup timed out for bootstrap - moving forward")
watch_task.cancel()

View File

@@ -1,6 +1,6 @@
{
"domain": "tplink",
"name": "TP-Link",
"integrations": ["tplink", "tplink_omada", "tplink_lte"],
"integrations": ["tplink", "tplink_omada", "tplink_lte", "tplink_tapo"],
"iot_standards": ["matter"]
}

View File

@@ -1,7 +1,6 @@
"""Adds config flow for AccuWeather."""
from __future__ import annotations
import asyncio
from asyncio import timeout
from typing import Any
@@ -61,7 +60,7 @@ class AccuWeatherFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
longitude=user_input[CONF_LONGITUDE],
)
await accuweather.async_get_location()
except (ApiError, ClientConnectorError, asyncio.TimeoutError, ClientError):
except (ApiError, ClientConnectorError, TimeoutError, ClientError):
errors["base"] = "cannot_connect"
except InvalidApiKeyError:
errors[CONF_API_KEY] = "invalid_api_key"

View File

@@ -1,7 +1,6 @@
"""Config flow for Rollease Acmeda Automate Pulse Hub."""
from __future__ import annotations
import asyncio
from asyncio import timeout
from contextlib import suppress
from typing import Any
@@ -42,7 +41,7 @@ class AcmedaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
}
hubs: list[aiopulse.Hub] = []
with suppress(asyncio.TimeoutError):
with suppress(TimeoutError):
async with timeout(5):
async for hub in aiopulse.Hub.discover():
if hub.id not in already_configured:

View File

@@ -303,7 +303,7 @@ class AdsEntity(Entity):
try:
async with timeout(10):
await self._event.wait()
except asyncio.TimeoutError:
except TimeoutError:
_LOGGER.debug("Variable %s: Timeout during first update", ads_var)
@property

View File

@@ -77,9 +77,8 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
new_data = entry.data.copy()
del new_data[CONF_RADIUS]
entry.version = 2
hass.config_entries.async_update_entry(
entry, data=new_data, options=new_options
entry, data=new_data, options=new_options, version=2
)
_LOGGER.info("Migration to version %s successful", entry.version)

View File

@@ -88,9 +88,13 @@ class AirtouchAC(CoordinatorEntity, ClimateEntity):
_attr_name = None
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_enable_turn_on_off_backwards_compatibility = False
def __init__(self, coordinator, ac_number, info):
"""Initialize the climate device."""
@@ -192,9 +196,14 @@ class AirtouchGroup(CoordinatorEntity, ClimateEntity):
_attr_has_entity_name = True
_attr_name = None
_attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_hvac_modes = AT_GROUP_MODES
_enable_turn_on_off_backwards_compatibility = False
def __init__(self, coordinator, group_number, info):
"""Initialize the climate device."""

View File

@@ -120,15 +120,12 @@ class Airtouch5ClimateEntity(ClimateEntity, Airtouch5Entity):
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_target_temperature_step = 1
_attr_name = None
_enable_turn_on_off_backwards_compatibility = False
class Airtouch5AC(Airtouch5ClimateEntity):
"""Representation of the AC unit. Used to control the overall HVAC Mode."""
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
)
def __init__(self, client: Airtouch5SimpleClient, ability: AcAbility) -> None:
"""Initialise the Climate Entity."""
super().__init__(client)
@@ -152,6 +149,14 @@ class Airtouch5AC(Airtouch5ClimateEntity):
if ability.supports_mode_heat:
self._attr_hvac_modes.append(HVACMode.HEAT)
self._attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE
)
if len(self.hvac_modes) > 1:
self._attr_supported_features |= (
ClimateEntityFeature.TURN_OFF | ClimateEntityFeature.TURN_ON
)
self._attr_fan_modes = []
if ability.supports_fan_speed_quiet:
self._attr_fan_modes.append(FAN_DIFFUSE)
@@ -262,7 +267,10 @@ class Airtouch5Zone(Airtouch5ClimateEntity):
_attr_hvac_modes = [HVACMode.OFF, HVACMode.FAN_ONLY]
_attr_preset_modes = [PRESET_NONE, PRESET_BOOST]
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.PRESET_MODE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
def __init__(

View File

@@ -242,7 +242,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# 1 -> 2: One geography per config entry
if version == 1:
version = entry.version = 2
version = 2
# Update the config entry to only include the first geography (there is always
# guaranteed to be at least one):
@@ -255,6 +255,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
unique_id=first_id,
title=f"Cloud API ({first_id})",
data={CONF_API_KEY: entry.data[CONF_API_KEY], **first_geography},
version=version,
)
# For any geographies that remain, create a new config entry for each one:
@@ -379,7 +380,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
},
)
else:
entry.version = version
hass.config_entries.async_update_entry(entry, version=version)
LOGGER.info("Migration to version %s successful", version)

View File

@@ -144,11 +144,6 @@ class AirzoneClimate(AirzoneEntity, ClimateEntity):
"""Define an Airzone Cloud climate."""
_attr_name = None
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_enable_turn_on_off_backwards_compatibility = False
@@ -180,6 +175,12 @@ class AirzoneClimate(AirzoneEntity, ClimateEntity):
class AirzoneDeviceClimate(AirzoneClimate):
"""Define an Airzone Cloud Device base class."""
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
async def async_turn_on(self) -> None:
"""Turn the entity on."""
params = {
@@ -217,6 +218,12 @@ class AirzoneDeviceClimate(AirzoneClimate):
class AirzoneDeviceGroupClimate(AirzoneClimate):
"""Define an Airzone Cloud DeviceGroup base class."""
_attr_supported_features = (
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
async def async_turn_on(self) -> None:
"""Turn the entity on."""
params = {

View File

@@ -1,5 +1,4 @@
"""The aladdin_connect component."""
import asyncio
import logging
from typing import Final
@@ -29,7 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
try:
await acc.login()
except (ClientError, asyncio.TimeoutError, Aladdin.ConnectionError) as ex:
except (ClientError, TimeoutError, Aladdin.ConnectionError) as ex:
raise ConfigEntryNotReady("Can not connect to host") from ex
except Aladdin.InvalidPasswordError as ex:
raise ConfigEntryAuthFailed("Incorrect Password") from ex

View File

@@ -1,7 +1,6 @@
"""Config flow for Aladdin Connect cover integration."""
from __future__ import annotations
import asyncio
from collections.abc import Mapping
from typing import Any
@@ -42,7 +41,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> None:
)
try:
await acc.login()
except (ClientError, asyncio.TimeoutError, Aladdin.ConnectionError) as ex:
except (ClientError, TimeoutError, Aladdin.ConnectionError) as ex:
raise ex
except Aladdin.InvalidPasswordError as ex:
@@ -81,7 +80,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
except InvalidAuth:
errors["base"] = "invalid_auth"
except (ClientError, asyncio.TimeoutError, Aladdin.ConnectionError):
except (ClientError, TimeoutError, Aladdin.ConnectionError):
errors["base"] = "cannot_connect"
else:
@@ -117,7 +116,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
except InvalidAuth:
errors["base"] = "invalid_auth"
except (ClientError, asyncio.TimeoutError, Aladdin.ConnectionError):
except (ClientError, TimeoutError, Aladdin.ConnectionError):
errors["base"] = "cannot_connect"
else:

View File

@@ -122,7 +122,7 @@ class Auth:
allow_redirects=True,
)
except (asyncio.TimeoutError, aiohttp.ClientError):
except (TimeoutError, aiohttp.ClientError):
_LOGGER.error("Timeout calling LWA to get auth token")
return None

View File

@@ -29,12 +29,20 @@ class AbstractConfig(ABC):
"""Initialize abstract config."""
self.hass = hass
self._enable_proactive_mode_lock = asyncio.Lock()
self._on_deinitialize: list[CALLBACK_TYPE] = []
async def async_initialize(self) -> None:
"""Perform async initialization of config."""
self._store = AlexaConfigStore(self.hass)
await self._store.async_load()
@callback
def async_deinitialize(self) -> None:
"""Remove listeners."""
_LOGGER.debug("async_deinitialize")
while self._on_deinitialize:
self._on_deinitialize.pop()()
@property
def supports_auth(self) -> bool:
"""Return if config supports auth."""

View File

@@ -1,7 +1,6 @@
"""Alexa state report code."""
from __future__ import annotations
import asyncio
from asyncio import timeout
from http import HTTPStatus
import json
@@ -375,7 +374,7 @@ async def async_send_changereport_message(
allow_redirects=True,
)
except (asyncio.TimeoutError, aiohttp.ClientError):
except (TimeoutError, aiohttp.ClientError):
_LOGGER.error("Timeout sending report to Alexa for %s", alexa_entity.entity_id)
return
@@ -531,7 +530,7 @@ async def async_send_doorbell_event_message(
allow_redirects=True,
)
except (asyncio.TimeoutError, aiohttp.ClientError):
except (TimeoutError, aiohttp.ClientError):
_LOGGER.error("Timeout sending report to Alexa for %s", alexa_entity.entity_id)
return

View File

@@ -3,18 +3,46 @@ from __future__ import annotations
import amberelectric
from amberelectric.api import amber_api
from amberelectric.model.site import Site
from amberelectric.model.site import Site, SiteStatus
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_API_TOKEN
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.selector import (
SelectOptionDict,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
)
from .const import CONF_SITE_ID, CONF_SITE_NAME, CONF_SITE_NMI, DOMAIN
from .const import CONF_SITE_ID, CONF_SITE_NAME, DOMAIN
API_URL = "https://app.amber.com.au/developers"
def generate_site_selector_name(site: Site) -> str:
"""Generate the name to show in the site drop down in the configuration flow."""
if site.status == SiteStatus.CLOSED:
return site.nmi + " (Closed: " + site.closed_on.isoformat() + ")" # type: ignore[no-any-return]
if site.status == SiteStatus.PENDING:
return site.nmi + " (Pending)" # type: ignore[no-any-return]
return site.nmi # type: ignore[no-any-return]
def filter_sites(sites: list[Site]) -> list[Site]:
"""Deduplicates the list of sites."""
filtered: list[Site] = []
filtered_nmi: set[str] = set()
for site in sorted(sites, key=lambda site: site.status.value):
if site.status == SiteStatus.ACTIVE or site.nmi not in filtered_nmi:
filtered.append(site)
filtered_nmi.add(site.nmi)
return filtered
class AmberElectricConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow."""
@@ -31,7 +59,7 @@ class AmberElectricConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
api: amber_api.AmberApi = amber_api.AmberApi.create(configuration)
try:
sites: list[Site] = api.get_sites()
sites: list[Site] = filter_sites(api.get_sites())
if len(sites) == 0:
self._errors[CONF_API_TOKEN] = "no_site"
return None
@@ -86,38 +114,31 @@ class AmberElectricConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
assert self._sites is not None
assert self._api_token is not None
api_token = self._api_token
if user_input is not None:
site_nmi = user_input[CONF_SITE_NMI]
sites = [site for site in self._sites if site.nmi == site_nmi]
site = sites[0]
site_id = site.id
site_id = user_input[CONF_SITE_ID]
name = user_input.get(CONF_SITE_NAME, site_id)
return self.async_create_entry(
title=name,
data={
CONF_SITE_ID: site_id,
CONF_API_TOKEN: api_token,
CONF_SITE_NMI: site.nmi,
},
data={CONF_SITE_ID: site_id, CONF_API_TOKEN: self._api_token},
)
user_input = {
CONF_API_TOKEN: api_token,
CONF_SITE_NMI: "",
CONF_SITE_NAME: "",
}
return self.async_show_form(
step_id="site",
data_schema=vol.Schema(
{
vol.Required(
CONF_SITE_NMI, default=user_input[CONF_SITE_NMI]
): vol.In([site.nmi for site in self._sites]),
vol.Optional(
CONF_SITE_NAME, default=user_input[CONF_SITE_NAME]
): str,
vol.Required(CONF_SITE_ID): SelectSelector(
SelectSelectorConfig(
options=[
SelectOptionDict(
value=site.id,
label=generate_site_selector_name(site),
)
for site in self._sites
],
mode=SelectSelectorMode.DROPDOWN,
)
),
vol.Optional(CONF_SITE_NAME): str,
}
),
errors=self._errors,

View File

@@ -6,7 +6,6 @@ from homeassistant.const import Platform
DOMAIN = "amberelectric"
CONF_SITE_NAME = "site_name"
CONF_SITE_ID = "site_id"
CONF_SITE_NMI = "site_nmi"
ATTRIBUTION = "Data provided by Amber Electric"

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/amberelectric",
"iot_class": "cloud_polling",
"loggers": ["amberelectric"],
"requirements": ["amberelectric==1.0.4"]
"requirements": ["amberelectric==1.1.0"]
}

View File

@@ -111,7 +111,8 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
en_reg = er.async_get(hass)
en_reg.async_clear_config_entry(entry.entry_id)
version = entry.version = 2
version = 2
hass.config_entries.async_update_entry(entry, version=version)
LOGGER.info("Migration to version %s successful", version)

View File

@@ -329,7 +329,7 @@ class Analytics:
response.status,
self.endpoint,
)
except asyncio.TimeoutError:
except TimeoutError:
LOGGER.error("Timeout sending analytics to %s", ANALYTICS_ENDPOINT_URL)
except aiohttp.ClientError as err:
LOGGER.error(

View File

@@ -3,11 +3,15 @@ from __future__ import annotations
from dataclasses import dataclass
from python_homeassistant_analytics import HomeassistantAnalyticsClient
from python_homeassistant_analytics import (
HomeassistantAnalyticsClient,
HomeassistantAnalyticsConnectionError,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_TRACKED_INTEGRATIONS, DOMAIN
@@ -28,7 +32,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Homeassistant Analytics from a config entry."""
client = HomeassistantAnalyticsClient(session=async_get_clientsession(hass))
integrations = await client.get_integrations()
try:
integrations = await client.get_integrations()
except HomeassistantAnalyticsConnectionError as ex:
raise ConfigEntryNotReady("Could not fetch integration list") from ex
names = {}
for integration in entry.options[CONF_TRACKED_INTEGRATIONS]:

View File

@@ -53,10 +53,25 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
) -> FlowResult:
"""Handle the initial step."""
self._async_abort_entries_match()
if user_input:
return self.async_create_entry(
title="Home Assistant Analytics Insights", data={}, options=user_input
)
errors: dict[str, str] = {}
if user_input is not None:
if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get(
CONF_TRACKED_CUSTOM_INTEGRATIONS
):
errors["base"] = "no_integrations_selected"
else:
return self.async_create_entry(
title="Home Assistant Analytics Insights",
data={},
options={
CONF_TRACKED_INTEGRATIONS: user_input.get(
CONF_TRACKED_INTEGRATIONS, []
),
CONF_TRACKED_CUSTOM_INTEGRATIONS: user_input.get(
CONF_TRACKED_CUSTOM_INTEGRATIONS, []
),
},
)
client = HomeassistantAnalyticsClient(
session=async_get_clientsession(self.hass)
@@ -78,16 +93,17 @@ class HomeassistantAnalyticsConfigFlow(ConfigFlow, domain=DOMAIN):
]
return self.async_show_form(
step_id="user",
errors=errors,
data_schema=vol.Schema(
{
vol.Required(CONF_TRACKED_INTEGRATIONS): SelectSelector(
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
SelectSelectorConfig(
options=options,
multiple=True,
sort=True,
)
),
vol.Required(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector(
vol.Optional(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector(
SelectSelectorConfig(
options=list(custom_integrations),
multiple=True,
@@ -106,8 +122,24 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Manage the options."""
if user_input:
return self.async_create_entry(title="", data=user_input)
errors: dict[str, str] = {}
if user_input is not None:
if not user_input.get(CONF_TRACKED_INTEGRATIONS) and not user_input.get(
CONF_TRACKED_CUSTOM_INTEGRATIONS
):
errors["base"] = "no_integrations_selected"
else:
return self.async_create_entry(
title="",
data={
CONF_TRACKED_INTEGRATIONS: user_input.get(
CONF_TRACKED_INTEGRATIONS, []
),
CONF_TRACKED_CUSTOM_INTEGRATIONS: user_input.get(
CONF_TRACKED_CUSTOM_INTEGRATIONS, []
),
},
)
client = HomeassistantAnalyticsClient(
session=async_get_clientsession(self.hass)
@@ -129,17 +161,18 @@ class HomeassistantAnalyticsOptionsFlowHandler(OptionsFlowWithConfigEntry):
]
return self.async_show_form(
step_id="init",
errors=errors,
data_schema=self.add_suggested_values_to_schema(
vol.Schema(
{
vol.Required(CONF_TRACKED_INTEGRATIONS): SelectSelector(
vol.Optional(CONF_TRACKED_INTEGRATIONS): SelectSelector(
SelectSelectorConfig(
options=options,
multiple=True,
sort=True,
)
),
vol.Required(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector(
vol.Optional(CONF_TRACKED_CUSTOM_INTEGRATIONS): SelectSelector(
SelectSelectorConfig(
options=list(custom_integrations),
multiple=True,

View File

@@ -10,6 +10,7 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -93,6 +94,7 @@ class HomeassistantAnalyticsSensor(
"""Home Assistant Analytics Sensor."""
_attr_has_entity_name = True
_attr_entity_category = EntityCategory.DIAGNOSTIC
entity_description: AnalyticsSensorEntityDescription

View File

@@ -3,25 +3,41 @@
"step": {
"user": {
"data": {
"tracked_integrations": "Integrations"
"tracked_integrations": "Integrations",
"tracked_custom_integrations": "Custom integrations"
},
"data_description": {
"tracked_integrations": "Select the integrations you want to track",
"tracked_custom_integrations": "Select the custom integrations you want to track"
}
}
},
"abort": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
},
"error": {
"no_integration_selected": "You must select at least one integration to track"
}
},
"options": {
"step": {
"init": {
"data": {
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_integrations%]"
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_integrations%]",
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data::tracked_custom_integrations%]"
},
"data_description": {
"tracked_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_integrations%]",
"tracked_custom_integrations": "[%key:component::analytics_insights::config::step::user::data_description::tracked_custom_integrations%]"
}
}
},
"abort": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"error": {
"no_integration_selected": "[%key:component::analytics_insights::config::error::no_integration_selected%]"
}
},
"entity": {

View File

@@ -1,7 +1,6 @@
"""The Android TV Remote integration."""
from __future__ import annotations
import asyncio
from asyncio import timeout
import logging
@@ -50,7 +49,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except InvalidAuth as exc:
# The Android TV is hard reset or the certificate and key files were deleted.
raise ConfigEntryAuthFailed from exc
except (CannotConnect, ConnectionClosed, asyncio.TimeoutError) as exc:
except (CannotConnect, ConnectionClosed, TimeoutError) as exc:
# The Android TV is network unreachable. Raise exception and let Home Assistant retry
# later. If device gets a new IP address the zeroconf flow will update the config.
raise ConfigEntryNotReady from exc

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aosmith",
"iot_class": "cloud_polling",
"requirements": ["py-aosmith==1.0.6"]
"requirements": ["py-aosmith==1.0.8"]
}

View File

@@ -1,7 +1,6 @@
"""Config flow for APCUPSd integration."""
from __future__ import annotations
import asyncio
from typing import Any
import voluptuous as vol
@@ -54,7 +53,7 @@ class ConfigFlowHandler(ConfigFlow, domain=DOMAIN):
coordinator = APCUPSdCoordinator(self.hass, host, port)
await coordinator.async_request_refresh()
if isinstance(coordinator.last_exception, (UpdateFailed, asyncio.TimeoutError)):
if isinstance(coordinator.last_exception, (UpdateFailed, TimeoutError)):
errors = {"base": "cannot_connect"}
return self.async_show_form(
step_id="user", data_schema=_SCHEMA, errors=errors

View File

@@ -175,7 +175,7 @@ class APIEventStream(HomeAssistantView):
msg = f"data: {payload}\n\n"
_LOGGER.debug("STREAM %s WRITING %s", id(stop_obj), msg.strip())
await response.write(msg.encode("UTF-8"))
except asyncio.TimeoutError:
except TimeoutError:
await to_write.put(STREAM_PING_PAYLOAD)
except asyncio.CancelledError:
@@ -222,7 +222,7 @@ class APIStatesView(HomeAssistantView):
if entity_perm(state.entity_id, "read")
)
response = web.Response(
body=b"[" + b",".join(states) + b"]",
body=b"".join((b"[", b",".join(states), b"]")),
content_type=CONTENT_TYPE_JSON,
zlib_executor_size=32768,
)

View File

@@ -1,8 +1,10 @@
"""The Apple TV integration."""
from __future__ import annotations
import asyncio
import logging
from random import randrange
from typing import TYPE_CHECKING, cast
from typing import Any, cast
from pyatv import connect, exceptions, scan
from pyatv.conf import AppleTV
@@ -25,8 +27,8 @@ from homeassistant.const import (
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
@@ -40,7 +42,8 @@ from .const import CONF_CREDENTIALS, CONF_IDENTIFIERS, CONF_START_OFF, DOMAIN
_LOGGER = logging.getLogger(__name__)
DEFAULT_NAME = "Apple TV"
DEFAULT_NAME_TV = "Apple TV"
DEFAULT_NAME_HP = "HomePod"
BACKOFF_TIME_LOWER_LIMIT = 15 # seconds
BACKOFF_TIME_UPPER_LIMIT = 300 # Five minutes
@@ -56,14 +59,39 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
manager = AppleTVManager(hass, entry)
if manager.is_on:
await manager.connect_once(raise_missing_credentials=True)
if not manager.atv:
address = entry.data[CONF_ADDRESS]
raise ConfigEntryNotReady(f"Not found at {address}, waiting for discovery")
address = entry.data[CONF_ADDRESS]
try:
await manager.async_first_connect()
except (
exceptions.AuthenticationError,
exceptions.InvalidCredentialsError,
exceptions.NoCredentialsError,
) as ex:
raise ConfigEntryAuthFailed(
f"{address}: Authentication failed, try reconfiguring device: {ex}"
) from ex
except (
asyncio.CancelledError,
exceptions.ConnectionLostError,
exceptions.ConnectionFailedError,
) as ex:
raise ConfigEntryNotReady(f"{address}: {ex}") from ex
except (
exceptions.ProtocolError,
exceptions.NoServiceError,
exceptions.PairingError,
exceptions.BackOffError,
exceptions.DeviceIdMissingError,
) as ex:
_LOGGER.debug(
"Error setting up apple_tv at %s: %s", address, ex, exc_info=ex
)
raise ConfigEntryNotReady(f"{address}: {ex}") from ex
hass.data.setdefault(DOMAIN, {})[entry.unique_id] = manager
async def on_hass_stop(event):
async def on_hass_stop(event: Event) -> None:
"""Stop push updates when hass stops."""
await manager.disconnect()
@@ -94,33 +122,29 @@ class AppleTVEntity(Entity):
_attr_should_poll = False
_attr_has_entity_name = True
_attr_name = None
atv: AppleTVInterface | None = None
def __init__(
self, name: str, identifier: str | None, manager: "AppleTVManager"
) -> None:
def __init__(self, name: str, identifier: str, manager: AppleTVManager) -> None:
"""Initialize device."""
self.atv: AppleTVInterface = None # type: ignore[assignment]
self.manager = manager
if TYPE_CHECKING:
assert identifier is not None
self._attr_unique_id = identifier
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, identifier)},
name=name,
)
async def async_added_to_hass(self):
async def async_added_to_hass(self) -> None:
"""Handle when an entity is about to be added to Home Assistant."""
@callback
def _async_connected(atv):
def _async_connected(atv: AppleTVInterface) -> None:
"""Handle that a connection was made to a device."""
self.atv = atv
self.async_device_connected(atv)
self.async_write_ha_state()
@callback
def _async_disconnected():
def _async_disconnected() -> None:
"""Handle that a connection to a device was lost."""
self.async_device_disconnected()
self.atv = None
@@ -143,10 +167,10 @@ class AppleTVEntity(Entity):
)
)
def async_device_connected(self, atv):
def async_device_connected(self, atv: AppleTVInterface) -> None:
"""Handle when connection is made to device."""
def async_device_disconnected(self):
def async_device_disconnected(self) -> None:
"""Handle when connection was lost to device."""
@@ -158,22 +182,23 @@ class AppleTVManager(DeviceListener):
in case of problems.
"""
atv: AppleTVInterface | None = None
_connection_attempts = 0
_connection_was_lost = False
_task: asyncio.Task[None] | None = None
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Initialize power manager."""
self.config_entry = config_entry
self.hass = hass
self.atv: AppleTVInterface | None = None
self.is_on = not config_entry.options.get(CONF_START_OFF, False)
self._connection_attempts = 0
self._connection_was_lost = False
self._task = None
async def init(self):
async def init(self) -> None:
"""Initialize power management."""
if self.is_on:
await self.connect()
def connection_lost(self, _):
def connection_lost(self, exception: Exception) -> None:
"""Device was unexpectedly disconnected.
This is a callback function from pyatv.interface.DeviceListener.
@@ -184,14 +209,14 @@ class AppleTVManager(DeviceListener):
self._connection_was_lost = True
self._handle_disconnect()
def connection_closed(self):
def connection_closed(self) -> None:
"""Device connection was (intentionally) closed.
This is a callback function from pyatv.interface.DeviceListener.
"""
self._handle_disconnect()
def _handle_disconnect(self):
def _handle_disconnect(self) -> None:
"""Handle that the device disconnected and restart connect loop."""
if self.atv:
self.atv.close()
@@ -199,12 +224,12 @@ class AppleTVManager(DeviceListener):
self._dispatch_send(SIGNAL_DISCONNECTED)
self._start_connect_loop()
async def connect(self):
async def connect(self) -> None:
"""Connect to device."""
self.is_on = True
self._start_connect_loop()
async def disconnect(self):
async def disconnect(self) -> None:
"""Disconnect from device."""
_LOGGER.debug("Disconnecting from device")
self.is_on = False
@@ -218,7 +243,7 @@ class AppleTVManager(DeviceListener):
except Exception: # pylint: disable=broad-except
_LOGGER.exception("An error occurred while disconnecting")
def _start_connect_loop(self):
def _start_connect_loop(self) -> None:
"""Start background connect loop to device."""
if not self._task and self.atv is None and self.is_on:
self._task = asyncio.create_task(self._connect_loop())
@@ -227,11 +252,25 @@ class AppleTVManager(DeviceListener):
"Not starting connect loop (%s, %s)", self.atv is None, self.is_on
)
async def _connect_once(self, raise_missing_credentials: bool) -> None:
"""Connect to device once."""
if conf := await self._scan():
await self._connect(conf, raise_missing_credentials)
async def async_first_connect(self) -> None:
"""Connect to device for the first time."""
connect_ok = False
try:
await self._connect_once(raise_missing_credentials=True)
connect_ok = True
finally:
if not connect_ok:
await self.disconnect()
async def connect_once(self, raise_missing_credentials: bool) -> None:
"""Try to connect once."""
try:
if conf := await self._scan():
await self._connect(conf, raise_missing_credentials)
await self._connect_once(raise_missing_credentials)
except exceptions.AuthenticationError:
self.config_entry.async_start_reauth(self.hass)
await self.disconnect()
@@ -244,9 +283,9 @@ class AppleTVManager(DeviceListener):
pass
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Failed to connect")
self.atv = None
await self.disconnect()
async def _connect_loop(self):
async def _connect_loop(self) -> None:
"""Connect loop background task function."""
_LOGGER.debug("Starting connect loop")
@@ -255,7 +294,8 @@ class AppleTVManager(DeviceListener):
while self.is_on and self.atv is None:
await self.connect_once(raise_missing_credentials=False)
if self.atv is not None:
break
# Calling self.connect_once may have set self.atv
break # type: ignore[unreachable]
self._connection_attempts += 1
backoff = min(
max(
@@ -352,13 +392,17 @@ class AppleTVManager(DeviceListener):
self._connection_was_lost = False
@callback
def _async_setup_device_registry(self):
def _async_setup_device_registry(self) -> None:
attrs = {
ATTR_IDENTIFIERS: {(DOMAIN, self.config_entry.unique_id)},
ATTR_MANUFACTURER: "Apple",
ATTR_NAME: self.config_entry.data[CONF_NAME],
}
attrs[ATTR_SUGGESTED_AREA] = attrs[ATTR_NAME].removesuffix(f" {DEFAULT_NAME}")
attrs[ATTR_SUGGESTED_AREA] = (
attrs[ATTR_NAME]
.removesuffix(f" {DEFAULT_NAME_TV}")
.removesuffix(f" {DEFAULT_NAME_HP}")
)
if self.atv:
dev_info = self.atv.device_info
@@ -379,18 +423,18 @@ class AppleTVManager(DeviceListener):
)
@property
def is_connecting(self):
def is_connecting(self) -> bool:
"""Return true if connection is in progress."""
return self._task is not None
def _address_updated(self, address):
def _address_updated(self, address: str) -> None:
"""Update cached address in config entry."""
_LOGGER.debug("Changing address to %s", address)
self.hass.config_entries.async_update_entry(
self.config_entry, data={**self.config_entry.data, CONF_ADDRESS: address}
)
def _dispatch_send(self, signal, *args):
def _dispatch_send(self, signal: str, *args: Any) -> None:
"""Dispatch a signal to all entities managed by this manager."""
async_dispatcher_send(
self.hass, f"{signal}_{self.config_entry.unique_id}", *args

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import asyncio
from collections import deque
from collections.abc import Mapping
from collections.abc import Awaitable, Callable, Mapping
from ipaddress import ip_address
import logging
from random import randrange
@@ -13,12 +13,13 @@ from pyatv import exceptions, pair, scan
from pyatv.const import DeviceModel, PairingRequirement, Protocol
from pyatv.convert import model_str, protocol_str
from pyatv.helpers import get_unique_id
from pyatv.interface import BaseConfig, PairingHandler
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.components import zeroconf
from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PIN
from homeassistant.core import callback
from homeassistant.core import HomeAssistant, callback
from homeassistant.data_entry_flow import AbortFlow, FlowResult
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -49,10 +50,12 @@ OPTIONS_FLOW = {
}
async def device_scan(hass, identifier, loop):
async def device_scan(
hass: HomeAssistant, identifier: str | None, loop: asyncio.AbstractEventLoop
) -> tuple[BaseConfig | None, list[str] | None]:
"""Scan for a specific device using identifier as filter."""
def _filter_device(dev):
def _filter_device(dev: BaseConfig) -> bool:
if identifier is None:
return True
if identifier == str(dev.address):
@@ -61,9 +64,12 @@ async def device_scan(hass, identifier, loop):
return True
return any(service.identifier == identifier for service in dev.services)
def _host_filter():
def _host_filter() -> list[str] | None:
if identifier is None:
return None
try:
return [ip_address(identifier)]
ip_address(identifier)
return [identifier]
except ValueError:
return None
@@ -84,6 +90,13 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
VERSION = 1
scan_filter: str | None = None
atv: BaseConfig | None = None
atv_identifiers: list[str] | None = None
protocol: Protocol | None = None
pairing: PairingHandler | None = None
protocols_to_pair: deque[Protocol] | None = None
@staticmethod
@callback
def async_get_options_flow(
@@ -92,18 +105,12 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Get options flow for this handler."""
return SchemaOptionsFlowHandler(config_entry, OPTIONS_FLOW)
def __init__(self):
def __init__(self) -> None:
"""Initialize a new AppleTVConfigFlow."""
self.scan_filter = None
self.atv = None
self.atv_identifiers = None
self.protocol = None
self.pairing = None
self.credentials = {} # Protocol -> credentials
self.protocols_to_pair = deque()
self.credentials: dict[int, str | None] = {} # Protocol -> credentials
@property
def device_identifier(self):
def device_identifier(self) -> str | None:
"""Return a identifier for the config entry.
A device has multiple unique identifiers, but Home Assistant only supports one
@@ -118,6 +125,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
existing config entry. If that's the case, the unique_id from that entry is
re-used, otherwise the newly discovered identifier is used instead.
"""
assert self.atv
all_identifiers = set(self.atv.all_identifiers)
if unique_id := self._entry_unique_id_from_identifers(all_identifiers):
return unique_id
@@ -143,7 +151,9 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.context["identifier"] = self.unique_id
return await self.async_step_reconfigure()
async def async_step_reconfigure(self, user_input=None):
async def async_step_reconfigure(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Inform user that reconfiguration is about to start."""
if user_input is not None:
return await self.async_find_device_wrapper(
@@ -152,7 +162,9 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_show_form(step_id="reconfigure")
async def async_step_user(self, user_input=None):
async def async_step_user(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Handle the initial step."""
errors = {}
if user_input is not None:
@@ -170,6 +182,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
await self.async_set_unique_id(
self.device_identifier, raise_on_progress=False
)
assert self.atv
self.context["all_identifiers"] = self.atv.all_identifiers
return await self.async_step_confirm()
@@ -275,8 +288,11 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
context["all_identifiers"].append(unique_id)
raise AbortFlow("already_in_progress")
async def async_found_zeroconf_device(self, user_input=None):
async def async_found_zeroconf_device(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Handle device found after Zeroconf discovery."""
assert self.atv
self.context["all_identifiers"] = self.atv.all_identifiers
# Also abort if an integration with this identifier already exists
await self.async_set_unique_id(self.device_identifier)
@@ -288,7 +304,11 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self.context["identifier"] = self.unique_id
return await self.async_step_confirm()
async def async_find_device_wrapper(self, next_func, allow_exist=False):
async def async_find_device_wrapper(
self,
next_func: Callable[[], Awaitable[FlowResult]],
allow_exist: bool = False,
) -> FlowResult:
"""Find a specific device and call another function when done.
This function will do error handling and bail out when an error
@@ -306,7 +326,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return await next_func()
async def async_find_device(self, allow_exist=False):
async def async_find_device(self, allow_exist: bool = False) -> None:
"""Scan for the selected device to discover services."""
self.atv, self.atv_identifiers = await device_scan(
self.hass, self.scan_filter, self.hass.loop
@@ -357,8 +377,11 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
if not allow_exist:
raise DeviceAlreadyConfigured()
async def async_step_confirm(self, user_input=None):
async def async_step_confirm(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Handle user-confirmation of discovered node."""
assert self.atv
if user_input is not None:
expected_identifier_count = len(self.context["all_identifiers"])
# If number of services found during device scan mismatch number of
@@ -384,7 +407,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
},
)
async def async_pair_next_protocol(self):
async def async_pair_next_protocol(self) -> FlowResult:
"""Start pairing process for the next available protocol."""
await self._async_cleanup()
@@ -393,8 +416,16 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return await self._async_get_entry()
self.protocol = self.protocols_to_pair.popleft()
assert self.atv
service = self.atv.get_service(self.protocol)
if service is None:
_LOGGER.debug(
"%s does not support pairing (cannot find a corresponding service)",
self.protocol,
)
return await self.async_pair_next_protocol()
# Service requires a password
if service.requires_password:
return await self.async_step_password()
@@ -413,7 +444,7 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
_LOGGER.debug("%s requires pairing", self.protocol)
# Protocol specific arguments
pair_args = {}
pair_args: dict[str, Any] = {}
if self.protocol in {Protocol.AirPlay, Protocol.Companion, Protocol.DMAP}:
pair_args["name"] = "Home Assistant"
if self.protocol == Protocol.DMAP:
@@ -448,8 +479,11 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return await self.async_step_pair_no_pin()
async def async_step_protocol_disabled(self, user_input=None):
async def async_step_protocol_disabled(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Inform user that a protocol is disabled and cannot be paired."""
assert self.protocol
if user_input is not None:
return await self.async_pair_next_protocol()
return self.async_show_form(
@@ -457,9 +491,13 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
description_placeholders={"protocol": protocol_str(self.protocol)},
)
async def async_step_pair_with_pin(self, user_input=None):
async def async_step_pair_with_pin(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Handle pairing step where a PIN is required from the user."""
errors = {}
assert self.pairing
assert self.protocol
if user_input is not None:
try:
self.pairing.pin(user_input[CONF_PIN])
@@ -480,8 +518,12 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
description_placeholders={"protocol": protocol_str(self.protocol)},
)
async def async_step_pair_no_pin(self, user_input=None):
async def async_step_pair_no_pin(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Handle step where user has to enter a PIN on the device."""
assert self.pairing
assert self.protocol
if user_input is not None:
await self.pairing.finish()
if self.pairing.has_paired:
@@ -497,12 +539,15 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
step_id="pair_no_pin",
description_placeholders={
"protocol": protocol_str(self.protocol),
"pin": pin,
"pin": str(pin),
},
)
async def async_step_service_problem(self, user_input=None):
async def async_step_service_problem(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Inform user that a service will not be added."""
assert self.protocol
if user_input is not None:
return await self.async_pair_next_protocol()
@@ -511,8 +556,11 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
description_placeholders={"protocol": protocol_str(self.protocol)},
)
async def async_step_password(self, user_input=None):
async def async_step_password(
self, user_input: dict[str, str] | None = None
) -> FlowResult:
"""Inform user that password is not supported."""
assert self.protocol
if user_input is not None:
return await self.async_pair_next_protocol()
@@ -521,18 +569,20 @@ class AppleTVConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
description_placeholders={"protocol": protocol_str(self.protocol)},
)
async def _async_cleanup(self):
async def _async_cleanup(self) -> None:
"""Clean up allocated resources."""
if self.pairing is not None:
await self.pairing.close()
self.pairing = None
async def _async_get_entry(self):
async def _async_get_entry(self) -> FlowResult:
"""Return config entry or update existing config entry."""
# Abort if no protocols were paired
if not self.credentials:
return self.async_abort(reason="setup_failed")
assert self.atv
data = {
CONF_NAME: self.atv.name,
CONF_CREDENTIALS: self.credentials,

View File

@@ -16,7 +16,15 @@ from pyatv.const import (
ShuffleState,
)
from pyatv.helpers import is_streamable
from pyatv.interface import AppleTV, Playing
from pyatv.interface import (
AppleTV,
AudioListener,
OutputDevice,
Playing,
PowerListener,
PushListener,
PushUpdater,
)
from homeassistant.components import media_source
from homeassistant.components.media_player import (
@@ -101,7 +109,9 @@ async def async_setup_entry(
async_add_entities([AppleTvMediaPlayer(name, config_entry.unique_id, manager)])
class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
class AppleTvMediaPlayer(
AppleTVEntity, MediaPlayerEntity, PowerListener, AudioListener, PushListener
):
"""Representation of an Apple TV media player."""
_attr_supported_features = SUPPORT_APPLE_TV
@@ -116,9 +126,9 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
def async_device_connected(self, atv: AppleTV) -> None:
"""Handle when connection is made to device."""
# NB: Do not use _is_feature_available here as it only works when playing
if self.atv.features.in_state(FeatureState.Available, FeatureName.PushUpdates):
self.atv.push_updater.listener = self
self.atv.push_updater.start()
if atv.features.in_state(FeatureState.Available, FeatureName.PushUpdates):
atv.push_updater.listener = self
atv.push_updater.start()
self._attr_supported_features = SUPPORT_BASE
@@ -126,7 +136,7 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
# "Unsupported" are considered here as the state of such a feature can never
# change after a connection has been established, i.e. an unsupported feature
# can never change to be supported.
all_features = self.atv.features.all_features()
all_features = atv.features.all_features()
for feature_name, support_flag in SUPPORT_FEATURE_MAPPING.items():
feature_info = all_features.get(feature_name)
if feature_info and feature_info.state != FeatureState.Unsupported:
@@ -136,16 +146,18 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
# metadata update arrives (sometime very soon after this callback returns)
# Listen to power updates
self.atv.power.listener = self
atv.power.listener = self
# Listen to volume updates
self.atv.audio.listener = self
atv.audio.listener = self
if self.atv.features.in_state(FeatureState.Available, FeatureName.AppList):
if atv.features.in_state(FeatureState.Available, FeatureName.AppList):
self.hass.create_task(self._update_app_list())
async def _update_app_list(self) -> None:
_LOGGER.debug("Updating app list")
if not self.atv:
return
try:
apps = await self.atv.apps.app_list()
except exceptions.NotSupportedError:
@@ -189,33 +201,56 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
return None
@callback
def playstatus_update(self, _, playing: Playing) -> None:
"""Print what is currently playing when it changes."""
self._playing = playing
def playstatus_update(self, updater: PushUpdater, playstatus: Playing) -> None:
"""Print what is currently playing when it changes.
This is a callback function from pyatv.interface.PushListener.
"""
self._playing = playstatus
self.async_write_ha_state()
@callback
def playstatus_error(self, _, exception: Exception) -> None:
"""Inform about an error and restart push updates."""
def playstatus_error(self, updater: PushUpdater, exception: Exception) -> None:
"""Inform about an error and restart push updates.
This is a callback function from pyatv.interface.PushListener.
"""
_LOGGER.warning("A %s error occurred: %s", exception.__class__, exception)
self._playing = None
self.async_write_ha_state()
@callback
def powerstate_update(self, old_state: PowerState, new_state: PowerState) -> None:
"""Update power state when it changes."""
"""Update power state when it changes.
This is a callback function from pyatv.interface.PowerListener.
"""
self.async_write_ha_state()
@callback
def volume_update(self, old_level: float, new_level: float) -> None:
"""Update volume when it changes."""
"""Update volume when it changes.
This is a callback function from pyatv.interface.AudioListener.
"""
self.async_write_ha_state()
@callback
def outputdevices_update(
self, old_devices: list[OutputDevice], new_devices: list[OutputDevice]
) -> None:
"""Output devices were updated.
This is a callback function from pyatv.interface.AudioListener.
"""
@property
def app_id(self) -> str | None:
"""ID of the current running app."""
if self._is_feature_available(FeatureName.App) and (
app := self.atv.metadata.app
if (
self.atv
and self._is_feature_available(FeatureName.App)
and (app := self.atv.metadata.app) is not None
):
return app.identifier
return None
@@ -223,8 +258,10 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
@property
def app_name(self) -> str | None:
"""Name of the current running app."""
if self._is_feature_available(FeatureName.App) and (
app := self.atv.metadata.app
if (
self.atv
and self._is_feature_available(FeatureName.App)
and (app := self.atv.metadata.app) is not None
):
return app.name
return None
@@ -255,7 +292,7 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
@property
def volume_level(self) -> float | None:
"""Volume level of the media player (0..1)."""
if self._is_feature_available(FeatureName.Volume):
if self.atv and self._is_feature_available(FeatureName.Volume):
return self.atv.audio.volume / 100.0 # from percent
return None
@@ -286,6 +323,8 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
"""Send the play_media command to the media player."""
# If input (file) has a file format supported by pyatv, then stream it with
# RAOP. Otherwise try to play it with regular AirPlay.
if not self.atv:
return
if media_type in {MediaType.APP, MediaType.URL}:
await self.atv.apps.launch_app(media_id)
return
@@ -313,7 +352,8 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
"""Hash value for media image."""
state = self.state
if (
self._playing
self.atv
and self._playing
and self._is_feature_available(FeatureName.Artwork)
and state not in {None, MediaPlayerState.OFF, MediaPlayerState.IDLE}
):
@@ -323,7 +363,11 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
async def async_get_media_image(self) -> tuple[bytes | None, str | None]:
"""Fetch media image of current playing image."""
state = self.state
if self._playing and state not in {MediaPlayerState.OFF, MediaPlayerState.IDLE}:
if (
self.atv
and self._playing
and state not in {MediaPlayerState.OFF, MediaPlayerState.IDLE}
):
artwork = await self.atv.metadata.artwork()
if artwork:
return artwork.bytes, artwork.mimetype
@@ -439,20 +483,24 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
async def async_turn_on(self) -> None:
"""Turn the media player on."""
if self._is_feature_available(FeatureName.TurnOn):
if self.atv and self._is_feature_available(FeatureName.TurnOn):
await self.atv.power.turn_on()
async def async_turn_off(self) -> None:
"""Turn the media player off."""
if (self._is_feature_available(FeatureName.TurnOff)) and (
not self._is_feature_available(FeatureName.PowerState)
or self.atv.power.power_state == PowerState.On
if (
self.atv
and (self._is_feature_available(FeatureName.TurnOff))
and (
not self._is_feature_available(FeatureName.PowerState)
or self.atv.power.power_state == PowerState.On
)
):
await self.atv.power.turn_off()
async def async_media_play_pause(self) -> None:
"""Pause media on media player."""
if self._playing:
if self.atv and self._playing:
await self.atv.remote_control.play_pause()
async def async_media_play(self) -> None:
@@ -519,5 +567,6 @@ class AppleTvMediaPlayer(AppleTVEntity, MediaPlayerEntity):
async def async_select_source(self, source: str) -> None:
"""Select input source."""
if app_id := self._app_list.get(source):
await self.atv.apps.launch_app(app_id)
if self.atv:
if app_id := self._app_list.get(source):
await self.atv.apps.launch_app(app_id)

View File

@@ -15,7 +15,7 @@ from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AppleTVEntity
from . import AppleTVEntity, AppleTVManager
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -38,8 +38,10 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Load Apple TV remote based on a config entry."""
name = config_entry.data[CONF_NAME]
manager = hass.data[DOMAIN][config_entry.unique_id]
name: str = config_entry.data[CONF_NAME]
# apple_tv config entries always have a unique id
assert config_entry.unique_id is not None
manager: AppleTVManager = hass.data[DOMAIN][config_entry.unique_id]
async_add_entities([AppleTVRemote(name, config_entry.unique_id, manager)])
@@ -47,7 +49,7 @@ class AppleTVRemote(AppleTVEntity, RemoteEntity):
"""Device that sends commands to an Apple TV."""
@property
def is_on(self):
def is_on(self) -> bool:
"""Return true if device is on."""
return self.atv is not None
@@ -64,13 +66,13 @@ class AppleTVRemote(AppleTVEntity, RemoteEntity):
num_repeats = kwargs[ATTR_NUM_REPEATS]
delay = kwargs.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS)
if not self.is_on:
if not self.atv:
_LOGGER.error("Unable to send commands, not connected to %s", self.name)
return
for _ in range(num_repeats):
for single_command in command:
attr_value = None
attr_value: Any = None
if attributes := COMMAND_TO_ATTRIBUTE.get(single_command):
attr_value = self.atv
for attr_name in attributes:
@@ -81,5 +83,5 @@ class AppleTVRemote(AppleTVEntity, RemoteEntity):
raise ValueError("Command not found. Exiting sequence")
_LOGGER.info("Sending command %s", single_command)
await attr_value() # type: ignore[operator]
await attr_value()
await asyncio.sleep(delay)

View File

@@ -0,0 +1,69 @@
"""The Aprilaire integration."""
from __future__ import annotations
import logging
from pyaprilaire.const import Attribute
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.device_registry import format_mac
from .const import DOMAIN
from .coordinator import AprilaireCoordinator
PLATFORMS: list[Platform] = [Platform.CLIMATE]
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry for Aprilaire."""
host = entry.data[CONF_HOST]
port = entry.data[CONF_PORT]
coordinator = AprilaireCoordinator(hass, entry.unique_id, host, port)
await coordinator.start_listen()
hass.data.setdefault(DOMAIN, {})[entry.unique_id] = coordinator
async def ready_callback(ready: bool):
if ready:
mac_address = format_mac(coordinator.data[Attribute.MAC_ADDRESS])
if mac_address != entry.unique_id:
raise ConfigEntryAuthFailed("Invalid MAC address")
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
async def _async_close(_: Event) -> None:
coordinator.stop_listen()
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_close)
)
else:
_LOGGER.error("Failed to wait for ready")
coordinator.stop_listen()
raise ConfigEntryNotReady()
await coordinator.wait_for_ready(ready_callback)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
coordinator: AprilaireCoordinator = hass.data[DOMAIN].pop(entry.unique_id)
coordinator.stop_listen()
return unload_ok

View File

@@ -0,0 +1,302 @@
"""The Aprilaire climate component."""
from __future__ import annotations
from typing import Any
from pyaprilaire.const import Attribute
from homeassistant.components.climate import (
FAN_AUTO,
FAN_ON,
PRESET_AWAY,
PRESET_NONE,
ClimateEntity,
ClimateEntityFeature,
HVACAction,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PRECISION_HALVES, PRECISION_WHOLE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
DOMAIN,
FAN_CIRCULATE,
PRESET_PERMANENT_HOLD,
PRESET_TEMPORARY_HOLD,
PRESET_VACATION,
)
from .coordinator import AprilaireCoordinator
from .entity import BaseAprilaireEntity
HVAC_MODE_MAP = {
1: HVACMode.OFF,
2: HVACMode.HEAT,
3: HVACMode.COOL,
4: HVACMode.HEAT,
5: HVACMode.AUTO,
}
HVAC_MODES_MAP = {
1: [HVACMode.OFF, HVACMode.HEAT],
2: [HVACMode.OFF, HVACMode.COOL],
3: [HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL],
4: [HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL],
5: [HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL, HVACMode.AUTO],
6: [HVACMode.OFF, HVACMode.HEAT, HVACMode.COOL, HVACMode.AUTO],
}
PRESET_MODE_MAP = {
1: PRESET_TEMPORARY_HOLD,
2: PRESET_PERMANENT_HOLD,
3: PRESET_AWAY,
4: PRESET_VACATION,
}
FAN_MODE_MAP = {
1: FAN_ON,
2: FAN_AUTO,
3: FAN_CIRCULATE,
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add climates for passed config_entry in HA."""
coordinator: AprilaireCoordinator = hass.data[DOMAIN][config_entry.unique_id]
async_add_entities([AprilaireClimate(coordinator, config_entry.unique_id)])
class AprilaireClimate(BaseAprilaireEntity, ClimateEntity):
"""Climate entity for Aprilaire."""
_attr_fan_modes = [FAN_AUTO, FAN_ON, FAN_CIRCULATE]
_attr_min_humidity = 10
_attr_max_humidity = 50
_attr_temperature_unit = UnitOfTemperature.CELSIUS
_attr_translation_key = "thermostat"
@property
def precision(self) -> float:
"""Get the precision based on the unit."""
return (
PRECISION_HALVES
if self.hass.config.units.temperature_unit == UnitOfTemperature.CELSIUS
else PRECISION_WHOLE
)
@property
def supported_features(self) -> ClimateEntityFeature:
"""Get supported features."""
features = 0
if self.coordinator.data.get(Attribute.MODE) == 5:
features = features | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
else:
features = features | ClimateEntityFeature.TARGET_TEMPERATURE
if self.coordinator.data.get(Attribute.HUMIDIFICATION_AVAILABLE) == 2:
features = features | ClimateEntityFeature.TARGET_HUMIDITY
features = features | ClimateEntityFeature.PRESET_MODE
features = features | ClimateEntityFeature.FAN_MODE
return features
@property
def current_humidity(self) -> int | None:
"""Get current humidity."""
return self.coordinator.data.get(
Attribute.INDOOR_HUMIDITY_CONTROLLING_SENSOR_VALUE
)
@property
def target_humidity(self) -> int | None:
"""Get current target humidity."""
return self.coordinator.data.get(Attribute.HUMIDIFICATION_SETPOINT)
@property
def hvac_mode(self) -> HVACMode | None:
"""Get HVAC mode."""
if mode := self.coordinator.data.get(Attribute.MODE):
if hvac_mode := HVAC_MODE_MAP.get(mode):
return hvac_mode
return None
@property
def hvac_modes(self) -> list[HVACMode]:
"""Get supported HVAC modes."""
if modes := self.coordinator.data.get(Attribute.THERMOSTAT_MODES):
if thermostat_modes := HVAC_MODES_MAP.get(modes):
return thermostat_modes
return []
@property
def hvac_action(self) -> HVACAction | None:
"""Get the current HVAC action."""
if self.coordinator.data.get(Attribute.HEATING_EQUIPMENT_STATUS, 0):
return HVACAction.HEATING
if self.coordinator.data.get(Attribute.COOLING_EQUIPMENT_STATUS, 0):
return HVACAction.COOLING
return HVACAction.IDLE
@property
def current_temperature(self) -> float | None:
"""Get current temperature."""
return self.coordinator.data.get(
Attribute.INDOOR_TEMPERATURE_CONTROLLING_SENSOR_VALUE
)
@property
def target_temperature(self) -> float | None:
"""Get the target temperature."""
hvac_mode = self.hvac_mode
if hvac_mode == HVACMode.COOL:
return self.target_temperature_high
if hvac_mode == HVACMode.HEAT:
return self.target_temperature_low
return None
@property
def target_temperature_step(self) -> float | None:
"""Get the step for the target temperature based on the unit."""
return (
0.5
if self.hass.config.units.temperature_unit == UnitOfTemperature.CELSIUS
else 1
)
@property
def target_temperature_high(self) -> float | None:
"""Get cool setpoint."""
return self.coordinator.data.get(Attribute.COOL_SETPOINT)
@property
def target_temperature_low(self) -> float | None:
"""Get heat setpoint."""
return self.coordinator.data.get(Attribute.HEAT_SETPOINT)
@property
def preset_mode(self) -> str | None:
"""Get the current preset mode."""
if hold := self.coordinator.data.get(Attribute.HOLD):
if preset_mode := PRESET_MODE_MAP.get(hold):
return preset_mode
return PRESET_NONE
@property
def preset_modes(self) -> list[str] | None:
"""Get the supported preset modes."""
presets = [PRESET_NONE, PRESET_VACATION]
if self.coordinator.data.get(Attribute.AWAY_AVAILABLE) == 1:
presets.append(PRESET_AWAY)
hold = self.coordinator.data.get(Attribute.HOLD, 0)
if hold == 1:
presets.append(PRESET_TEMPORARY_HOLD)
elif hold == 2:
presets.append(PRESET_PERMANENT_HOLD)
return presets
@property
def fan_mode(self) -> str | None:
"""Get fan mode."""
if mode := self.coordinator.data.get(Attribute.FAN_MODE):
if fan_mode := FAN_MODE_MAP.get(mode):
return fan_mode
return None
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
cool_setpoint = 0
heat_setpoint = 0
if temperature := kwargs.get("temperature"):
if self.coordinator.data.get(Attribute.MODE) == 3:
cool_setpoint = temperature
else:
heat_setpoint = temperature
else:
if target_temp_low := kwargs.get("target_temp_low"):
heat_setpoint = target_temp_low
if target_temp_high := kwargs.get("target_temp_high"):
cool_setpoint = target_temp_high
if cool_setpoint == 0 and heat_setpoint == 0:
return
await self.coordinator.client.update_setpoint(cool_setpoint, heat_setpoint)
await self.coordinator.client.read_control()
async def async_set_humidity(self, humidity: int) -> None:
"""Set the target humidification setpoint."""
await self.coordinator.client.set_humidification_setpoint(humidity)
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set the fan mode."""
try:
fan_mode_value_index = list(FAN_MODE_MAP.values()).index(fan_mode)
except ValueError as exc:
raise ValueError(f"Unsupported fan mode {fan_mode}") from exc
fan_mode_value = list(FAN_MODE_MAP.keys())[fan_mode_value_index]
await self.coordinator.client.update_fan_mode(fan_mode_value)
await self.coordinator.client.read_control()
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the HVAC mode."""
try:
mode_value_index = list(HVAC_MODE_MAP.values()).index(hvac_mode)
except ValueError as exc:
raise ValueError(f"Unsupported HVAC mode {hvac_mode}") from exc
mode_value = list(HVAC_MODE_MAP.keys())[mode_value_index]
await self.coordinator.client.update_mode(mode_value)
await self.coordinator.client.read_control()
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set the preset mode."""
if preset_mode == PRESET_AWAY:
await self.coordinator.client.set_hold(3)
elif preset_mode == PRESET_VACATION:
await self.coordinator.client.set_hold(4)
elif preset_mode == PRESET_NONE:
await self.coordinator.client.set_hold(0)
else:
raise ValueError(f"Unsupported preset mode {preset_mode}")
await self.coordinator.client.read_scheduling()

View File

@@ -0,0 +1,72 @@
"""Config flow for the Aprilaire integration."""
from __future__ import annotations
import logging
from typing import Any
from pyaprilaire.const import Attribute
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_HOST, CONF_PORT
from homeassistant.data_entry_flow import FlowResult
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import format_mac
from .const import DOMAIN
from .coordinator import AprilaireCoordinator
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=7000): cv.port,
}
)
_LOGGER = logging.getLogger(__name__)
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Aprilaire."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
"""Handle the initial step."""
if user_input is None:
return self.async_show_form(
step_id="user", data_schema=STEP_USER_DATA_SCHEMA
)
coordinator = AprilaireCoordinator(
self.hass, None, user_input[CONF_HOST], user_input[CONF_PORT]
)
await coordinator.start_listen()
async def ready_callback(ready: bool):
if not ready:
_LOGGER.error("Failed to wait for ready")
try:
ready = await coordinator.wait_for_ready(ready_callback)
finally:
coordinator.stop_listen()
mac_address = coordinator.data.get(Attribute.MAC_ADDRESS)
if ready and mac_address is not None:
await self.async_set_unique_id(format_mac(mac_address))
self._abort_if_unique_id_configured()
return self.async_create_entry(title="Aprilaire", data=user_input)
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors={"base": "connection_failed"},
)

View File

@@ -0,0 +1,11 @@
"""Constants for the Aprilaire integration."""
from __future__ import annotations
DOMAIN = "aprilaire"
FAN_CIRCULATE = "Circulate"
PRESET_TEMPORARY_HOLD = "Temporary"
PRESET_PERMANENT_HOLD = "Permanent"
PRESET_VACATION = "Vacation"

View File

@@ -0,0 +1,209 @@
"""The Aprilaire coordinator."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
import logging
from typing import Any, Optional
import pyaprilaire.client
from pyaprilaire.const import MODELS, Attribute, FunctionalDomain
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import BaseDataUpdateCoordinatorProtocol
from .const import DOMAIN
RECONNECT_INTERVAL = 60 * 60
RETRY_CONNECTION_INTERVAL = 10
WAIT_TIMEOUT = 30
_LOGGER = logging.getLogger(__name__)
class AprilaireCoordinator(BaseDataUpdateCoordinatorProtocol):
"""Coordinator for interacting with the thermostat."""
def __init__(
self,
hass: HomeAssistant,
unique_id: str | None,
host: str,
port: int,
) -> None:
"""Initialize the coordinator."""
self.hass = hass
self.unique_id = unique_id
self.data: dict[str, Any] = {}
self._listeners: dict[CALLBACK_TYPE, tuple[CALLBACK_TYPE, object | None]] = {}
self.client = pyaprilaire.client.AprilaireClient(
host,
port,
self.async_set_updated_data,
_LOGGER,
RECONNECT_INTERVAL,
RETRY_CONNECTION_INTERVAL,
)
if hasattr(self.client, "data") and self.client.data:
self.data = self.client.data
@callback
def async_add_listener(
self, update_callback: CALLBACK_TYPE, context: Any = None
) -> Callable[[], None]:
"""Listen for data updates."""
@callback
def remove_listener() -> None:
"""Remove update listener."""
self._listeners.pop(remove_listener)
self._listeners[remove_listener] = (update_callback, context)
return remove_listener
@callback
def async_update_listeners(self) -> None:
"""Update all registered listeners."""
for update_callback, _ in list(self._listeners.values()):
update_callback()
def async_set_updated_data(self, data: Any) -> None:
"""Manually update data, notify listeners and reset refresh interval."""
old_device_info = self.create_device_info(self.data)
self.data = self.data | data
self.async_update_listeners()
new_device_info = self.create_device_info(data)
if (
old_device_info is not None
and new_device_info is not None
and old_device_info != new_device_info
):
device_registry = dr.async_get(self.hass)
device = device_registry.async_get_device(old_device_info["identifiers"])
if device is not None:
new_device_info.pop("identifiers", None)
new_device_info.pop("connections", None)
device_registry.async_update_device(
device_id=device.id,
**new_device_info, # type: ignore[misc]
)
async def start_listen(self):
"""Start listening for data."""
await self.client.start_listen()
def stop_listen(self):
"""Stop listening for data."""
self.client.stop_listen()
async def wait_for_ready(
self, ready_callback: Callable[[bool], Awaitable[bool]]
) -> bool:
"""Wait for the client to be ready."""
if not self.data or Attribute.MAC_ADDRESS not in self.data:
data = await self.client.wait_for_response(
FunctionalDomain.IDENTIFICATION, 2, WAIT_TIMEOUT
)
if not data or Attribute.MAC_ADDRESS not in data:
_LOGGER.error("Missing MAC address")
await ready_callback(False)
return False
if not self.data or Attribute.NAME not in self.data:
await self.client.wait_for_response(
FunctionalDomain.IDENTIFICATION, 4, WAIT_TIMEOUT
)
if not self.data or Attribute.THERMOSTAT_MODES not in self.data:
await self.client.wait_for_response(
FunctionalDomain.CONTROL, 7, WAIT_TIMEOUT
)
if (
not self.data
or Attribute.INDOOR_TEMPERATURE_CONTROLLING_SENSOR_STATUS not in self.data
):
await self.client.wait_for_response(
FunctionalDomain.SENSORS, 2, WAIT_TIMEOUT
)
await ready_callback(True)
return True
@property
def device_name(self) -> str:
"""Get the name of the thermostat."""
return self.create_device_name(self.data)
def create_device_name(self, data: Optional[dict[str, Any]]) -> str:
"""Create the name of the thermostat."""
name = data.get(Attribute.NAME) if data else None
return name if name else "Aprilaire"
def get_hw_version(self, data: dict[str, Any]) -> str:
"""Get the hardware version."""
if hardware_revision := data.get(Attribute.HARDWARE_REVISION):
return (
f"Rev. {chr(hardware_revision)}"
if hardware_revision > ord("A")
else str(hardware_revision)
)
return "Unknown"
@property
def device_info(self) -> DeviceInfo | None:
"""Get the device info for the thermostat."""
return self.create_device_info(self.data)
def create_device_info(self, data: dict[str, Any]) -> DeviceInfo | None:
"""Create the device info for the thermostat."""
if data is None or Attribute.MAC_ADDRESS not in data or self.unique_id is None:
return None
device_info = DeviceInfo(
identifiers={(DOMAIN, self.unique_id)},
name=self.create_device_name(data),
manufacturer="Aprilaire",
)
model_number = data.get(Attribute.MODEL_NUMBER)
if model_number is not None:
device_info["model"] = MODELS.get(model_number, f"Unknown ({model_number})")
device_info["hw_version"] = self.get_hw_version(data)
firmware_major_revision = data.get(Attribute.FIRMWARE_MAJOR_REVISION)
firmware_minor_revision = data.get(Attribute.FIRMWARE_MINOR_REVISION)
if firmware_major_revision is not None:
device_info["sw_version"] = (
str(firmware_major_revision)
if firmware_minor_revision is None
else f"{firmware_major_revision}.{firmware_minor_revision:02}"
)
return device_info

View File

@@ -0,0 +1,46 @@
"""Base functionality for Aprilaire entities."""
from __future__ import annotations
import logging
from pyaprilaire.const import Attribute
from homeassistant.helpers.update_coordinator import BaseCoordinatorEntity
from .coordinator import AprilaireCoordinator
_LOGGER = logging.getLogger(__name__)
class BaseAprilaireEntity(BaseCoordinatorEntity[AprilaireCoordinator]):
"""Base for Aprilaire entities."""
_attr_available = False
_attr_has_entity_name = True
def __init__(
self, coordinator: AprilaireCoordinator, unique_id: str | None
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._attr_device_info = coordinator.device_info
self._attr_unique_id = f"{unique_id}_{self.translation_key}"
self._update_available()
def _update_available(self):
"""Update the entity availability."""
connected: bool = self.coordinator.data.get(
Attribute.CONNECTED, None
) or self.coordinator.data.get(Attribute.RECONNECTING, None)
stopped: bool = self.coordinator.data.get(Attribute.STOPPED, None)
self._attr_available = connected and not stopped
async def async_update(self) -> None:
"""Implement abstract base method."""

View File

@@ -0,0 +1,11 @@
{
"domain": "aprilaire",
"name": "Aprilaire",
"codeowners": ["@chamberlain2007"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/aprilaire",
"integration_type": "device",
"iot_class": "local_push",
"loggers": ["pyaprilaire"],
"requirements": ["pyaprilaire==0.7.0"]
}

View File

@@ -0,0 +1,28 @@
{
"config": {
"step": {
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",
"port": "[%key:common::config_flow::data::port%]"
},
"data_description": {
"port": "Usually 7000 or 8000"
}
}
},
"error": {
"connection_failed": "Connection failed. Please check that the host and port is correct."
},
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
}
},
"entity": {
"climate": {
"thermostat": {
"name": "Thermostat"
}
}
}
}

View File

@@ -83,7 +83,7 @@ async def _run_client(hass: HomeAssistant, client: Client, interval: float) -> N
except ConnectionFailed:
await asyncio.sleep(interval)
except asyncio.TimeoutError:
except TimeoutError:
continue
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception, aborting arcam client")

View File

@@ -241,7 +241,7 @@ async def websocket_run(
# Task contains a timeout
async with asyncio.timeout(timeout):
await run_task
except asyncio.TimeoutError:
except TimeoutError:
pipeline_input.run.process_event(
PipelineEvent(
PipelineEventType.ERROR,
@@ -487,7 +487,7 @@ async def websocket_device_capture(
)
try:
with contextlib.suppress(asyncio.TimeoutError):
with contextlib.suppress(TimeoutError):
async with asyncio.timeout(timeout_seconds):
while True:
# Send audio chunks encoded as base64

View File

@@ -11,6 +11,7 @@ from typing import Any, TypeVar, cast
from aioasuswrt.asuswrt import AsusWrt as AsusWrtLegacy
from aiohttp import ClientSession
from pyasuswrt import AsusWrtError, AsusWrtHttp
from pyasuswrt.exceptions import AsusWrtNotAvailableInfoError
from homeassistant.const import (
CONF_HOST,
@@ -354,13 +355,14 @@ class AsusWrtHttpBridge(AsusWrtBridge):
async def async_get_available_sensors(self) -> dict[str, dict[str, Any]]:
"""Return a dictionary of available sensors for this bridge."""
sensors_temperatures = await self._get_available_temperature_sensors()
sensors_loadavg = await self._get_loadavg_sensors_availability()
sensors_types = {
SENSORS_TYPE_BYTES: {
KEY_SENSORS: SENSORS_BYTES,
KEY_METHOD: self._get_bytes,
},
SENSORS_TYPE_LOAD_AVG: {
KEY_SENSORS: SENSORS_LOAD_AVG,
KEY_SENSORS: sensors_loadavg,
KEY_METHOD: self._get_load_avg,
},
SENSORS_TYPE_RATES: {
@@ -393,6 +395,16 @@ class AsusWrtHttpBridge(AsusWrtBridge):
return []
return available_sensors
async def _get_loadavg_sensors_availability(self) -> list[str]:
"""Check if load avg is available on the router."""
try:
await self._api.async_get_loadavg()
except AsusWrtNotAvailableInfoError:
return []
except AsusWrtError:
pass
return SENSORS_LOAD_AVG
@handle_errors_and_zip(AsusWrtError, SENSORS_BYTES)
async def _get_bytes(self) -> Any:
"""Fetch byte information from the router."""

View File

@@ -59,7 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return await async_setup_august(hass, entry, august_gateway)
except (RequireValidation, InvalidAuth) as err:
raise ConfigEntryAuthFailed from err
except asyncio.TimeoutError as err:
except TimeoutError as err:
raise ConfigEntryNotReady("Timed out connecting to august api") from err
except (AugustApiAIOHTTPError, ClientResponseError, CannotConnect) as err:
raise ConfigEntryNotReady from err
@@ -233,7 +233,7 @@ class AugustData(AugustSubscriberMixin):
return_exceptions=True,
):
if isinstance(result, Exception) and not isinstance(
result, (asyncio.TimeoutError, ClientResponseError, CannotConnect)
result, (TimeoutError, ClientResponseError, CannotConnect)
):
_LOGGER.warning(
"Unexpected exception during initial sync: %s",
@@ -249,10 +249,11 @@ class AugustData(AugustSubscriberMixin):
device = self.get_device_detail(device_id)
activities = activities_from_pubnub_message(device, date_time, message)
activity_stream = self.activity_stream
if activities:
activity_stream.async_process_newer_device_activities(activities)
if activities and activity_stream.async_process_newer_device_activities(
activities
):
self.async_signal_device_id_update(device.device_id)
activity_stream.async_schedule_house_id_refresh(device.house_id)
activity_stream.async_schedule_house_id_refresh(device.house_id)
@callback
def async_stop(self) -> None:
@@ -292,7 +293,7 @@ class AugustData(AugustSubscriberMixin):
for device_id in device_ids_list:
try:
await self._async_refresh_device_detail_by_id(device_id)
except asyncio.TimeoutError:
except TimeoutError:
_LOGGER.warning(
"Timed out calling august api during refresh of device: %s",
device_id,

View File

@@ -28,5 +28,5 @@
"documentation": "https://www.home-assistant.io/integrations/august",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==1.10.0", "yalexs-ble==2.4.0"]
"requirements": ["yalexs==1.11.2", "yalexs-ble==2.4.1"]
}

View File

@@ -1,7 +1,6 @@
"""Helpers to resolve client ID/secret."""
from __future__ import annotations
import asyncio
from html.parser import HTMLParser
from ipaddress import ip_address
import logging
@@ -102,7 +101,7 @@ async def fetch_redirect_uris(hass: HomeAssistant, url: str) -> list[str]:
if chunks == 10:
break
except asyncio.TimeoutError:
except TimeoutError:
_LOGGER.error("Timeout while looking up redirect_uri %s", url)
except aiohttp.client_exceptions.ClientSSLError:
_LOGGER.error("SSL error while looking up redirect_uri %s", url)

View File

@@ -50,7 +50,7 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) ->
if config_entry.version != 3:
# Home Assistant 2023.2
config_entry.version = 3
hass.config_entries.async_update_entry(config_entry, version=3)
_LOGGER.info("Migration to version %s successful", config_entry.version)

View File

@@ -1,6 +1,5 @@
"""Axis network device abstraction."""
import asyncio
from asyncio import timeout
from types import MappingProxyType
from typing import Any
@@ -270,7 +269,7 @@ async def get_axis_device(
)
raise AuthenticationRequired from err
except (asyncio.TimeoutError, axis.RequestError) as err:
except (TimeoutError, axis.RequestError) as err:
LOGGER.error("Error connecting to the Axis device at %s", config[CONF_HOST])
raise CannotConnect from err

View File

@@ -4,11 +4,12 @@ from __future__ import annotations
import asyncio
from dataclasses import asdict, dataclass
import hashlib
import io
import json
from pathlib import Path
import tarfile
from tarfile import TarError
from tempfile import TemporaryDirectory
import time
from typing import Any, Protocol, cast
from securetar import SecureTarFile, atomic_contents_add
@@ -17,7 +18,7 @@ from homeassistant.const import __version__ as HAVERSION
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import integration_platform
from homeassistant.helpers.json import save_json
from homeassistant.helpers.json import json_bytes
from homeassistant.util import dt as dt_util
from homeassistant.util.json import json_loads_object
@@ -81,6 +82,38 @@ class BackupManager:
return
self.platforms[integration_domain] = platform
async def pre_backup_actions(self) -> None:
"""Perform pre backup actions."""
if not self.loaded_platforms:
await self.load_platforms()
pre_backup_results = await asyncio.gather(
*(
platform.async_pre_backup(self.hass)
for platform in self.platforms.values()
),
return_exceptions=True,
)
for result in pre_backup_results:
if isinstance(result, Exception):
raise result
async def post_backup_actions(self) -> None:
"""Perform post backup actions."""
if not self.loaded_platforms:
await self.load_platforms()
post_backup_results = await asyncio.gather(
*(
platform.async_post_backup(self.hass)
for platform in self.platforms.values()
),
return_exceptions=True,
)
for result in post_backup_results:
if isinstance(result, Exception):
raise result
async def load_backups(self) -> None:
"""Load data of stored backup files."""
backups = await self.hass.async_add_executor_job(self._read_backups)
@@ -159,22 +192,9 @@ class BackupManager:
if self.backing_up:
raise HomeAssistantError("Backup already in progress")
if not self.loaded_platforms:
await self.load_platforms()
try:
self.backing_up = True
pre_backup_results = await asyncio.gather(
*(
platform.async_pre_backup(self.hass)
for platform in self.platforms.values()
),
return_exceptions=True,
)
for result in pre_backup_results:
if isinstance(result, Exception):
raise result
await self.pre_backup_actions()
backup_name = f"Core {HAVERSION}"
date_str = dt_util.now().isoformat()
slug = _generate_slug(date_str, backup_name)
@@ -207,16 +227,7 @@ class BackupManager:
return backup
finally:
self.backing_up = False
post_backup_results = await asyncio.gather(
*(
platform.async_post_backup(self.hass)
for platform in self.platforms.values()
),
return_exceptions=True,
)
for result in post_backup_results:
if isinstance(result, Exception):
raise result
await self.post_backup_actions()
def _mkdir_and_generate_backup_contents(
self,
@@ -228,18 +239,18 @@ class BackupManager:
LOGGER.debug("Creating backup directory")
self.backup_dir.mkdir()
with TemporaryDirectory() as tmp_dir, SecureTarFile(
outer_secure_tarfile = SecureTarFile(
tar_file_path, "w", gzip=False, bufsize=BUF_SIZE
) as tar_file:
tmp_dir_path = Path(tmp_dir)
save_json(
tmp_dir_path.joinpath("./backup.json").as_posix(),
backup_data,
)
with SecureTarFile(
tmp_dir_path.joinpath("./homeassistant.tar.gz").as_posix(),
"w",
bufsize=BUF_SIZE,
)
with outer_secure_tarfile as outer_secure_tarfile_tarfile:
raw_bytes = json_bytes(backup_data)
fileobj = io.BytesIO(raw_bytes)
tar_info = tarfile.TarInfo(name="./backup.json")
tar_info.size = len(raw_bytes)
tar_info.mtime = int(time.time())
outer_secure_tarfile_tarfile.addfile(tar_info, fileobj=fileobj)
with outer_secure_tarfile.create_inner_tar(
"./homeassistant.tar.gz", gzip=True
) as core_tar:
atomic_contents_add(
tar_file=core_tar,
@@ -247,7 +258,7 @@ class BackupManager:
excludes=EXCLUDE_FROM_BACKUP,
arcname="data",
)
tar_file.add(tmp_dir_path, arcname=".")
return tar_file_path.stat().st_size

View File

@@ -7,5 +7,5 @@
"integration_type": "system",
"iot_class": "calculated",
"quality_scale": "internal",
"requirements": ["securetar==2023.3.0"]
"requirements": ["securetar==2024.2.0"]
}

View File

@@ -1,7 +1,6 @@
"""The Big Ass Fans integration."""
from __future__ import annotations
import asyncio
from asyncio import timeout
from aiobafi6 import Device, Service
@@ -42,7 +41,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
raise ConfigEntryNotReady(
f"Unexpected device found at {ip_address}; expected {entry.unique_id}, found {device.dns_sd_uuid}"
) from ex
except asyncio.TimeoutError as ex:
except TimeoutError as ex:
run_future.cancel()
raise ConfigEntryNotReady(f"Timed out connecting to {ip_address}") from ex

View File

@@ -1,7 +1,6 @@
"""Config flow for baf."""
from __future__ import annotations
import asyncio
from asyncio import timeout
import logging
from typing import Any
@@ -28,7 +27,7 @@ async def async_try_connect(ip_address: str) -> Device:
try:
async with timeout(RUN_TIMEOUT):
await device.async_wait_available()
except asyncio.TimeoutError as ex:
except TimeoutError as ex:
raise CannotConnect from ex
finally:
run_future.cancel()

View File

@@ -2,7 +2,6 @@
from __future__ import annotations
import logging
import socket
from pyblackbird import get_blackbird
from serial import SerialException
@@ -93,7 +92,7 @@ def setup_platform(
try:
blackbird = get_blackbird(host, False)
connection = host
except socket.timeout:
except TimeoutError:
_LOGGER.error("Error connecting to the Blackbird controller")
return

View File

@@ -62,7 +62,6 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
def __init__(self, feature: blebox_uniapi.light.Light) -> None:
"""Initialize a BleBox light."""
super().__init__(feature)
self._attr_supported_color_modes = {self.color_mode}
if feature.effect_list:
self._attr_supported_features = LightEntityFeature.EFFECT
@@ -94,6 +93,11 @@ class BleBoxLightEntity(BleBoxEntity[blebox_uniapi.light.Light], LightEntity):
return color_mode_tmp
@property
def supported_color_modes(self):
"""Return supported color modes."""
return {self.color_mode}
@property
def effect_list(self) -> list[str]:
"""Return the list of supported effects."""

View File

@@ -1,5 +1,4 @@
"""Support for Blink Home Camera System."""
import asyncio
from copy import deepcopy
import logging
@@ -93,7 +92,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
try:
await blink.start()
except (ClientError, asyncio.TimeoutError) as ex:
except (ClientError, TimeoutError) as ex:
raise ConfigEntryNotReady("Can not connect to host") from ex
if blink.auth.check_key_required():

View File

@@ -1,7 +1,6 @@
"""Support for Blink Alarm Control Panel."""
from __future__ import annotations
import asyncio
import logging
from blinkpy.blinkpy import Blink, BlinkSyncModule
@@ -91,7 +90,7 @@ class BlinkSyncModuleHA(
try:
await self.sync.async_arm(False)
except asyncio.TimeoutError as er:
except TimeoutError as er:
raise HomeAssistantError("Blink failed to disarm camera") from er
await self.coordinator.async_refresh()
@@ -101,7 +100,7 @@ class BlinkSyncModuleHA(
try:
await self.sync.async_arm(True)
except asyncio.TimeoutError as er:
except TimeoutError as er:
raise HomeAssistantError("Blink failed to arm camera away") from er
await self.coordinator.async_refresh()

View File

@@ -1,7 +1,6 @@
"""Support for Blink system camera."""
from __future__ import annotations
import asyncio
from collections.abc import Mapping
import contextlib
import logging
@@ -96,7 +95,7 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera):
try:
await self._camera.async_arm(True)
except asyncio.TimeoutError as er:
except TimeoutError as er:
raise HomeAssistantError("Blink failed to arm camera") from er
self._camera.motion_enabled = True
@@ -106,7 +105,7 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera):
"""Disable motion detection for the camera."""
try:
await self._camera.async_arm(False)
except asyncio.TimeoutError as er:
except TimeoutError as er:
raise HomeAssistantError("Blink failed to disarm camera") from er
self._camera.motion_enabled = False
@@ -124,7 +123,7 @@ class BlinkCamera(CoordinatorEntity[BlinkUpdateCoordinator], Camera):
async def trigger_camera(self) -> None:
"""Trigger camera to take a snapshot."""
with contextlib.suppress(asyncio.TimeoutError):
with contextlib.suppress(TimeoutError):
await self._camera.snap_picture()
self.async_write_ha_state()

View File

@@ -1,7 +1,6 @@
"""Support for Blink Motion detection switches."""
from __future__ import annotations
import asyncio
from typing import Any
from homeassistant.components.switch import (
@@ -74,7 +73,7 @@ class BlinkSwitch(CoordinatorEntity[BlinkUpdateCoordinator], SwitchEntity):
try:
await self._camera.async_arm(True)
except asyncio.TimeoutError as er:
except TimeoutError as er:
raise HomeAssistantError(
"Blink failed to arm camera motion detection"
) from er
@@ -86,7 +85,7 @@ class BlinkSwitch(CoordinatorEntity[BlinkUpdateCoordinator], SwitchEntity):
try:
await self._camera.async_arm(False)
except asyncio.TimeoutError as er:
except TimeoutError as er:
raise HomeAssistantError(
"Blink failed to dis-arm camera motion detection"
) from er

View File

@@ -1,6 +1,7 @@
"""The Blue Current integration."""
from __future__ import annotations
import asyncio
from contextlib import suppress
from datetime import datetime
from typing import Any
@@ -14,8 +15,13 @@ from bluecurrent_api.exceptions import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_NAME, CONF_API_TOKEN, Platform
from homeassistant.core import HomeAssistant
from homeassistant.const import (
ATTR_NAME,
CONF_API_TOKEN,
EVENT_HOMEASSISTANT_STOP,
Platform,
)
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.event import async_call_later
@@ -47,7 +53,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
except BlueCurrentException as err:
raise ConfigEntryNotReady from err
hass.async_create_task(connector.start_loop())
hass.async_create_background_task(connector.start_loop(), "blue_current-websocket")
await client.get_charge_points()
await client.wait_for_response()
@@ -56,6 +62,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
config_entry.async_on_unload(connector.disconnect)
async def _async_disconnect_websocket(_: Event) -> None:
await connector.disconnect()
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_disconnect_websocket)
return True
@@ -78,9 +89,9 @@ class Connector:
self, hass: HomeAssistant, config: ConfigEntry, client: Client
) -> None:
"""Initialize."""
self.config: ConfigEntry = config
self.hass: HomeAssistant = hass
self.client: Client = client
self.config = config
self.hass = hass
self.client = client
self.charge_points: dict[str, dict] = {}
self.grid: dict[str, Any] = {}
self.available = False
@@ -93,22 +104,12 @@ class Connector:
async def on_data(self, message: dict) -> None:
"""Handle received data."""
async def handle_charge_points(data: list) -> None:
"""Loop over the charge points and get their data."""
for entry in data:
evse_id = entry[EVSE_ID]
model = entry[MODEL_TYPE]
name = entry[ATTR_NAME]
self.add_charge_point(evse_id, model, name)
await self.get_charge_point_data(evse_id)
await self.client.get_grid_status(data[0][EVSE_ID])
object_name: str = message[OBJECT]
# gets charge point ids
if object_name == CHARGE_POINTS:
charge_points_data: list = message[DATA]
await handle_charge_points(charge_points_data)
await self.handle_charge_point_data(charge_points_data)
# gets charge point key / values
elif object_name in VALUE_TYPES:
@@ -122,8 +123,21 @@ class Connector:
self.grid = data
self.dispatch_grid_update_signal()
async def get_charge_point_data(self, evse_id: str) -> None:
"""Get all the data of a charge point."""
async def handle_charge_point_data(self, charge_points_data: list) -> None:
"""Handle incoming chargepoint data."""
await asyncio.gather(
*(
self.handle_charge_point(
entry[EVSE_ID], entry[MODEL_TYPE], entry[ATTR_NAME]
)
for entry in charge_points_data
)
)
await self.client.get_grid_status(charge_points_data[0][EVSE_ID])
async def handle_charge_point(self, evse_id: str, model: str, name: str) -> None:
"""Add the chargepoint and request their data."""
self.add_charge_point(evse_id, model, name)
await self.client.get_status(evse_id)
def add_charge_point(self, evse_id: str, model: str, name: str) -> None:
@@ -159,9 +173,8 @@ class Connector:
"""Keep trying to reconnect to the websocket."""
try:
await self.connect(self.config.data[CONF_API_TOKEN])
LOGGER.info("Reconnected to the Blue Current websocket")
LOGGER.debug("Reconnected to the Blue Current websocket")
self.hass.async_create_task(self.start_loop())
await self.client.get_charge_points()
except RequestLimitReached:
self.available = False
async_call_later(

View File

@@ -1,4 +1,6 @@
"""Entity representing a Blue Current charge point."""
from abc import abstractmethod
from homeassistant.const import ATTR_NAME
from homeassistant.core import callback
from homeassistant.helpers.device_registry import DeviceInfo
@@ -17,9 +19,9 @@ class BlueCurrentEntity(Entity):
def __init__(self, connector: Connector, signal: str) -> None:
"""Initialize the entity."""
self.connector: Connector = connector
self.signal: str = signal
self.has_value: bool = False
self.connector = connector
self.signal = signal
self.has_value = False
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
@@ -40,9 +42,9 @@ class BlueCurrentEntity(Entity):
return self.connector.available and self.has_value
@callback
@abstractmethod
def update_from_latest_data(self) -> None:
"""Update the entity from the latest data."""
raise NotImplementedError
class ChargepointEntity(BlueCurrentEntity):
@@ -50,6 +52,8 @@ class ChargepointEntity(BlueCurrentEntity):
def __init__(self, connector: Connector, evse_id: str) -> None:
"""Initialize the entity."""
super().__init__(connector, f"{DOMAIN}_value_update_{evse_id}")
chargepoint_name = connector.charge_points[evse_id][ATTR_NAME]
self.evse_id = evse_id
@@ -59,5 +63,3 @@ class ChargepointEntity(BlueCurrentEntity):
manufacturer="Blue Current",
model=connector.charge_points[evse_id][MODEL_TYPE],
)
super().__init__(connector, f"{DOMAIN}_value_update_{self.evse_id}")

View File

@@ -13,7 +13,6 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"limit_reached": "Request limit reached",
"invalid_token": "Invalid token",
"no_cards_found": "No charge cards found",
"already_connected": "Already connected",
"unknown": "[%key:common::config_flow::error::unknown%]"
},

View File

@@ -290,7 +290,7 @@ class BluesoundPlayer(MediaPlayerEntity):
while True:
await self.async_update_status()
except (asyncio.TimeoutError, ClientError, BluesoundPlayer._TimeoutException):
except (TimeoutError, ClientError, BluesoundPlayer._TimeoutException):
_LOGGER.info("Node %s:%s is offline, retrying later", self.name, self.port)
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
self.start_polling()
@@ -317,7 +317,7 @@ class BluesoundPlayer(MediaPlayerEntity):
self._retry_remove = None
await self.force_update_sync_status(self._init_callback, True)
except (asyncio.TimeoutError, ClientError):
except (TimeoutError, ClientError):
_LOGGER.info("Node %s:%s is offline, retrying later", self.host, self.port)
self._retry_remove = async_track_time_interval(
self._hass, self.async_init, NODE_RETRY_INITIATION
@@ -370,7 +370,7 @@ class BluesoundPlayer(MediaPlayerEntity):
_LOGGER.error("Error %s on %s", response.status, url)
return None
except (asyncio.TimeoutError, aiohttp.ClientError):
except (TimeoutError, aiohttp.ClientError):
if raise_timeout:
_LOGGER.info("Timeout: %s:%s", self.host, self.port)
raise
@@ -437,7 +437,7 @@ class BluesoundPlayer(MediaPlayerEntity):
"Error %s on %s. Trying one more time", response.status, url
)
except (asyncio.TimeoutError, ClientError):
except (TimeoutError, ClientError):
self._is_online = False
self._last_status_update = None
self._status = None

View File

@@ -90,6 +90,8 @@ def seen_all_fields(
class IntegrationMatcher:
"""Integration matcher for the bluetooth integration."""
__slots__ = ("_integration_matchers", "_matched", "_matched_connectable", "_index")
def __init__(self, integration_matchers: list[BluetoothMatcher]) -> None:
"""Initialize the matcher."""
self._integration_matchers = integration_matchers
@@ -159,6 +161,16 @@ class BluetoothMatcherIndexBase(Generic[_T]):
any bucket and we can quickly reject the service info as not matching.
"""
__slots__ = (
"local_name",
"service_uuid",
"service_data_uuid",
"manufacturer_id",
"service_uuid_set",
"service_data_uuid_set",
"manufacturer_id_set",
)
def __init__(self) -> None:
"""Initialize the matcher index."""
self.local_name: dict[str, list[_T]] = {}
@@ -285,6 +297,8 @@ class BluetoothCallbackMatcherIndex(
Supports matching on addresses.
"""
__slots__ = ("address", "connectable")
def __init__(self) -> None:
"""Initialize the matcher index."""
super().__init__()

View File

@@ -649,7 +649,8 @@ class PassiveBluetoothProcessorEntity(Entity, Generic[_PassiveBluetoothDataProce
self._attr_device_info[ATTR_NAME] = self.processor.coordinator.name
if device_id is None:
self._attr_device_info[ATTR_CONNECTIONS] = {(CONNECTION_BLUETOOTH, address)}
self._attr_name = processor.entity_names.get(entity_key)
if (name := processor.entity_names.get(entity_key)) is not None:
self._attr_name = name
@property
def available(self) -> bool:

View File

@@ -1,7 +1,6 @@
"""Tracking for bluetooth low energy devices."""
from __future__ import annotations
import asyncio
from datetime import datetime, timedelta
import logging
from uuid import UUID
@@ -155,7 +154,7 @@ async def async_setup_scanner( # noqa: C901
async with BleakClient(device) as client:
bat_char = await client.read_gatt_char(BATTERY_CHARACTERISTIC_UUID)
battery = ord(bat_char)
except asyncio.TimeoutError:
except TimeoutError:
_LOGGER.debug(
"Timeout when trying to get battery status for %s", service_info.name
)

View File

@@ -10,7 +10,11 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE_ID, CONF_ENTITY_ID, CONF_NAME, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import discovery, entity_registry as er
from homeassistant.helpers import (
device_registry as dr,
discovery,
entity_registry as er,
)
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.typing import ConfigType
@@ -146,6 +150,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
)
)
# Clean up vehicles which are not assigned to the account anymore
account_vehicles = {(DOMAIN, v.vin) for v in coordinator.account.vehicles}
device_registry = dr.async_get(hass)
device_entries = dr.async_entries_for_config_entry(
device_registry, config_entry_id=entry.entry_id
)
for device in device_entries:
if not device.identifiers.intersection(account_vehicles):
device_registry.async_update_device(
device.id, remove_config_entry_id=entry.entry_id
)
return True

View File

@@ -1,5 +1,4 @@
"""The Bond integration."""
from asyncio import TimeoutError as AsyncIOTimeoutError
from http import HTTPStatus
import logging
from typing import Any
@@ -56,7 +55,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.error("Bond token no longer valid: %s", ex)
return False
raise ConfigEntryNotReady from ex
except (ClientError, AsyncIOTimeoutError, OSError) as error:
except (ClientError, TimeoutError, OSError) as error:
raise ConfigEntryNotReady from error
bpup_subs = BPUPSubscriptions()

View File

@@ -1,7 +1,6 @@
"""Config flow for Bond integration."""
from __future__ import annotations
import asyncio
import contextlib
from http import HTTPStatus
import logging
@@ -87,7 +86,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
try:
if not (token := await async_get_token(self.hass, host)):
return
except asyncio.TimeoutError:
except TimeoutError:
return
self._discovered[CONF_ACCESS_TOKEN] = token

View File

@@ -2,7 +2,7 @@
from __future__ import annotations
from abc import abstractmethod
from asyncio import Lock, TimeoutError as AsyncIOTimeoutError
from asyncio import Lock
from datetime import datetime
import logging
@@ -139,7 +139,7 @@ class BondEntity(Entity):
"""Fetch via the API."""
try:
state: dict = await self._hub.bond.device_state(self._device_id)
except (ClientError, AsyncIOTimeoutError, OSError) as error:
except (ClientError, TimeoutError, OSError) as error:
if self.available:
_LOGGER.warning(
"Entity %s has become unavailable", self.entity_id, exc_info=error

View File

@@ -3,8 +3,8 @@ from __future__ import annotations
import logging
from python_bring_api.bring import Bring
from python_bring_api.exceptions import (
from bring_api.bring import Bring
from bring_api.exceptions import (
BringAuthException,
BringParseException,
BringRequestException,
@@ -14,6 +14,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import DOMAIN
from .coordinator import BringDataUpdateCoordinator
@@ -29,14 +30,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
email = entry.data[CONF_EMAIL]
password = entry.data[CONF_PASSWORD]
bring = Bring(email, password)
def login_and_load_lists() -> None:
bring.login()
bring.loadLists()
session = async_get_clientsession(hass)
bring = Bring(session, email, password)
try:
await hass.async_add_executor_job(login_and_load_lists)
await bring.login()
await bring.loadLists()
except BringRequestException as e:
raise ConfigEntryNotReady(
f"Timeout while connecting for email '{email}'"

View File

@@ -4,13 +4,14 @@ from __future__ import annotations
import logging
from typing import Any
from python_bring_api.bring import Bring
from python_bring_api.exceptions import BringAuthException, BringRequestException
from bring_api.bring import Bring
from bring_api.exceptions import BringAuthException, BringRequestException
import voluptuous as vol
from homeassistant import config_entries
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
TextSelector,
TextSelectorConfig,
@@ -48,14 +49,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
bring = Bring(user_input[CONF_EMAIL], user_input[CONF_PASSWORD])
def login_and_load_lists() -> None:
bring.login()
bring.loadLists()
session = async_get_clientsession(self.hass)
bring = Bring(session, user_input[CONF_EMAIL], user_input[CONF_PASSWORD])
try:
await self.hass.async_add_executor_job(login_and_load_lists)
await bring.login()
await bring.loadLists()
except BringRequestException:
errors["base"] = "cannot_connect"
except BringAuthException:

View File

@@ -4,9 +4,9 @@ from __future__ import annotations
from datetime import timedelta
import logging
from python_bring_api.bring import Bring
from python_bring_api.exceptions import BringParseException, BringRequestException
from python_bring_api.types import BringItemsResponse, BringList
from bring_api.bring import Bring
from bring_api.exceptions import BringParseException, BringRequestException
from bring_api.types import BringItemsResponse, BringList
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -40,9 +40,7 @@ class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]):
async def _async_update_data(self) -> dict[str, BringData]:
try:
lists_response = await self.hass.async_add_executor_job(
self.bring.loadLists
)
lists_response = await self.bring.loadLists()
except BringRequestException as e:
raise UpdateFailed("Unable to connect and retrieve data from bring") from e
except BringParseException as e:
@@ -51,9 +49,7 @@ class BringDataUpdateCoordinator(DataUpdateCoordinator[dict[str, BringData]]):
list_dict = {}
for lst in lists_response["lists"]:
try:
items = await self.hass.async_add_executor_job(
self.bring.getItems, lst["listUuid"]
)
items = await self.bring.getItems(lst["listUuid"])
except BringRequestException as e:
raise UpdateFailed(
"Unable to connect and retrieve data from bring"

View File

@@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/bring",
"integration_type": "service",
"iot_class": "cloud_polling",
"requirements": ["python-bring-api==2.0.0"]
"requirements": ["bring-api==0.1.1"]
}

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from python_bring_api.exceptions import BringRequestException
from bring_api.exceptions import BringRequestException
from homeassistant.components.todo import (
TodoItem,
@@ -75,8 +75,8 @@ class BringTodoListEntity(
"""Return the todo items."""
return [
TodoItem(
uid=item["name"],
summary=item["name"],
uid=item["itemId"],
summary=item["itemId"],
description=item["specification"] or "",
status=TodoItemStatus.NEEDS_ACTION,
)
@@ -91,11 +91,8 @@ class BringTodoListEntity(
async def async_create_todo_item(self, item: TodoItem) -> None:
"""Add an item to the To-do list."""
try:
await self.hass.async_add_executor_job(
self.coordinator.bring.saveItem,
self.bring_list["listUuid"],
item.summary,
item.description or "",
await self.coordinator.bring.saveItem(
self.bring_list["listUuid"], item.summary, item.description or ""
)
except BringRequestException as e:
raise HomeAssistantError("Unable to save todo item for bring") from e
@@ -126,16 +123,14 @@ class BringTodoListEntity(
assert item.uid
if item.status == TodoItemStatus.COMPLETED:
await self.hass.async_add_executor_job(
self.coordinator.bring.removeItem,
await self.coordinator.bring.removeItem(
bring_list["listUuid"],
item.uid,
)
elif item.summary == item.uid:
try:
await self.hass.async_add_executor_job(
self.coordinator.bring.updateItem,
await self.coordinator.bring.updateItem(
bring_list["listUuid"],
item.uid,
item.description or "",
@@ -144,13 +139,11 @@ class BringTodoListEntity(
raise HomeAssistantError("Unable to update todo item for bring") from e
else:
try:
await self.hass.async_add_executor_job(
self.coordinator.bring.removeItem,
await self.coordinator.bring.removeItem(
bring_list["listUuid"],
item.uid,
)
await self.hass.async_add_executor_job(
self.coordinator.bring.saveItem,
await self.coordinator.bring.saveItem(
bring_list["listUuid"],
item.summary,
item.description or "",
@@ -164,8 +157,8 @@ class BringTodoListEntity(
"""Delete an item from the To-do list."""
for uid in uids:
try:
await self.hass.async_add_executor_job(
self.coordinator.bring.removeItem, self.bring_list["listUuid"], uid
await self.coordinator.bring.removeItem(
self.bring_list["listUuid"], uid
)
except BringRequestException as e:
raise HomeAssistantError("Unable to delete todo item for bring") from e

View File

@@ -0,0 +1,105 @@
{
"entity": {
"sensor": {
"belt_unit_remaining_life": {
"default": "mdi:current-ac"
},
"black_drum_page_counter": {
"default": "mdi:chart-donut"
},
"black_drum_remaining_life": {
"default": "mdi:chart-donut"
},
"black_drum_remaining_pages": {
"default": "mdi:chart-donut"
},
"black_toner_remaining": {
"default": "mdi:printer-3d-nozzle"
},
"black_ink_remaining": {
"default": "mdi:printer-3d-nozzle"
},
"bw_pages": {
"default": "mdi:file-document-outline"
},
"color_pages": {
"default": "mdi:file-document-outline"
},
"cyan_drum_page_counter": {
"default": "mdi:chart-donut"
},
"cyan_drum_remaining_life": {
"default": "mdi:chart-donut"
},
"cyan_drum_remaining_pages": {
"default": "mdi:chart-donut"
},
"cyan_ink_remaining": {
"default": "mdi:printer-3d-nozzle"
},
"cyan_toner_remaining": {
"default": "mdi:printer-3d-nozzle"
},
"drum_page_counter": {
"default": "mdi:chart-donut"
},
"drum_remaining_life": {
"default": "mdi:chart-donut"
},
"drum_remaining_pages": {
"default": "mdi:chart-donut"
},
"duplex_unit_page_counter": {
"default": "mdi:file-document-outline"
},
"fuser_remaining_life": {
"default": "mdi:water-outline"
},
"laser_remaining_life": {
"default": "mdi:spotlight-beam"
},
"magenta_drum_page_counter": {
"default": "mdi:chart-donut"
},
"magenta_drum_remaining_life": {
"default": "mdi:chart-donut"
},
"magenta_drum_remaining_pages": {
"default": "mdi:chart-donut"
},
"magenta_ink_remaining": {
"default": "mdi:printer-3d-nozzle"
},
"magenta_toner_remaining": {
"default": "mdi:printer-3d-nozzle"
},
"status": {
"default": "mdi:printer"
},
"page_counter": {
"default": "mdi:file-document-outline"
},
"pf_kit_1_remaining_life": {
"default": "mdi:printer-3d"
},
"pf_kit_mp_remaining_life": {
"default": "mdi:printer-3d"
},
"yellow_drum_page_counter": {
"default": "mdi:chart-donut"
},
"yellow_drum_remaining_life": {
"default": "mdi:chart-donut"
},
"yellow_drum_remaining_pages": {
"default": "mdi:chart-donut"
},
"yellow_ink_remaining": {
"default": "mdi:printer-3d-nozzle"
},
"yellow_toner_remaining": {
"default": "mdi:printer-3d-nozzle"
}
}
}
}

View File

@@ -52,14 +52,12 @@ class BrotherSensorEntityDescription(
SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
BrotherSensorEntityDescription(
key="status",
icon="mdi:printer",
translation_key="status",
entity_category=EntityCategory.DIAGNOSTIC,
value=lambda data: data.status,
),
BrotherSensorEntityDescription(
key="page_counter",
icon="mdi:file-document-outline",
translation_key="page_counter",
native_unit_of_measurement=UNIT_PAGES,
state_class=SensorStateClass.MEASUREMENT,
@@ -68,7 +66,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="bw_counter",
icon="mdi:file-document-outline",
translation_key="bw_pages",
native_unit_of_measurement=UNIT_PAGES,
state_class=SensorStateClass.MEASUREMENT,
@@ -77,7 +74,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="color_counter",
icon="mdi:file-document-outline",
translation_key="color_pages",
native_unit_of_measurement=UNIT_PAGES,
state_class=SensorStateClass.MEASUREMENT,
@@ -86,7 +82,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="duplex_unit_pages_counter",
icon="mdi:file-document-outline",
translation_key="duplex_unit_page_counter",
native_unit_of_measurement=UNIT_PAGES,
state_class=SensorStateClass.MEASUREMENT,
@@ -95,7 +90,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="drum_remaining_life",
icon="mdi:chart-donut",
translation_key="drum_remaining_life",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
@@ -104,7 +98,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="drum_remaining_pages",
icon="mdi:chart-donut",
translation_key="drum_remaining_pages",
native_unit_of_measurement=UNIT_PAGES,
state_class=SensorStateClass.MEASUREMENT,
@@ -113,7 +106,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="drum_counter",
icon="mdi:chart-donut",
translation_key="drum_page_counter",
native_unit_of_measurement=UNIT_PAGES,
state_class=SensorStateClass.MEASUREMENT,
@@ -122,7 +114,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="black_drum_remaining_life",
icon="mdi:chart-donut",
translation_key="black_drum_remaining_life",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
@@ -131,7 +122,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="black_drum_remaining_pages",
icon="mdi:chart-donut",
translation_key="black_drum_remaining_pages",
native_unit_of_measurement=UNIT_PAGES,
state_class=SensorStateClass.MEASUREMENT,
@@ -140,7 +130,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="black_drum_counter",
icon="mdi:chart-donut",
translation_key="black_drum_page_counter",
native_unit_of_measurement=UNIT_PAGES,
state_class=SensorStateClass.MEASUREMENT,
@@ -149,7 +138,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="cyan_drum_remaining_life",
icon="mdi:chart-donut",
translation_key="cyan_drum_remaining_life",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
@@ -158,7 +146,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="cyan_drum_remaining_pages",
icon="mdi:chart-donut",
translation_key="cyan_drum_remaining_pages",
native_unit_of_measurement=UNIT_PAGES,
state_class=SensorStateClass.MEASUREMENT,
@@ -167,7 +154,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="cyan_drum_counter",
icon="mdi:chart-donut",
translation_key="cyan_drum_page_counter",
native_unit_of_measurement=UNIT_PAGES,
state_class=SensorStateClass.MEASUREMENT,
@@ -176,7 +162,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="magenta_drum_remaining_life",
icon="mdi:chart-donut",
translation_key="magenta_drum_remaining_life",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
@@ -185,7 +170,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="magenta_drum_remaining_pages",
icon="mdi:chart-donut",
translation_key="magenta_drum_remaining_pages",
native_unit_of_measurement=UNIT_PAGES,
state_class=SensorStateClass.MEASUREMENT,
@@ -194,7 +178,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="magenta_drum_counter",
icon="mdi:chart-donut",
translation_key="magenta_drum_page_counter",
native_unit_of_measurement=UNIT_PAGES,
state_class=SensorStateClass.MEASUREMENT,
@@ -203,7 +186,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="yellow_drum_remaining_life",
icon="mdi:chart-donut",
translation_key="yellow_drum_remaining_life",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
@@ -212,7 +194,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="yellow_drum_remaining_pages",
icon="mdi:chart-donut",
translation_key="yellow_drum_remaining_pages",
native_unit_of_measurement=UNIT_PAGES,
state_class=SensorStateClass.MEASUREMENT,
@@ -221,7 +202,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="yellow_drum_counter",
icon="mdi:chart-donut",
translation_key="yellow_drum_page_counter",
native_unit_of_measurement=UNIT_PAGES,
state_class=SensorStateClass.MEASUREMENT,
@@ -230,7 +210,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="belt_unit_remaining_life",
icon="mdi:current-ac",
translation_key="belt_unit_remaining_life",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
@@ -239,7 +218,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="fuser_remaining_life",
icon="mdi:water-outline",
translation_key="fuser_remaining_life",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
@@ -248,7 +226,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="laser_remaining_life",
icon="mdi:spotlight-beam",
translation_key="laser_remaining_life",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
@@ -257,7 +234,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="pf_kit_1_remaining_life",
icon="mdi:printer-3d",
translation_key="pf_kit_1_remaining_life",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
@@ -266,7 +242,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="pf_kit_mp_remaining_life",
icon="mdi:printer-3d",
translation_key="pf_kit_mp_remaining_life",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
@@ -275,7 +250,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="black_toner_remaining",
icon="mdi:printer-3d-nozzle",
translation_key="black_toner_remaining",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
@@ -284,7 +258,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="cyan_toner_remaining",
icon="mdi:printer-3d-nozzle",
translation_key="cyan_toner_remaining",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
@@ -293,7 +266,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="magenta_toner_remaining",
icon="mdi:printer-3d-nozzle",
translation_key="magenta_toner_remaining",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
@@ -302,7 +274,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="yellow_toner_remaining",
icon="mdi:printer-3d-nozzle",
translation_key="yellow_toner_remaining",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
@@ -311,7 +282,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="black_ink_remaining",
icon="mdi:printer-3d-nozzle",
translation_key="black_ink_remaining",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
@@ -320,7 +290,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="cyan_ink_remaining",
icon="mdi:printer-3d-nozzle",
translation_key="cyan_ink_remaining",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
@@ -329,7 +298,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="magenta_ink_remaining",
icon="mdi:printer-3d-nozzle",
translation_key="magenta_ink_remaining",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
@@ -338,7 +306,6 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
),
BrotherSensorEntityDescription(
key="yellow_ink_remaining",
icon="mdi:printer-3d-nozzle",
translation_key="yellow_ink_remaining",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,

View File

@@ -128,7 +128,7 @@ class BuienradarCam(Camera):
_LOGGER.debug("HTTP 200 - Last-Modified: %s", last_modified)
return True
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
except (TimeoutError, aiohttp.ClientError) as err:
_LOGGER.error("Failed to fetch image, %s", type(err))
return False

View File

@@ -1,5 +1,4 @@
"""Shared utilities for different supported platforms."""
import asyncio
from asyncio import timeout
from datetime import datetime, timedelta
from http import HTTPStatus
@@ -104,7 +103,7 @@ class BrData:
result[MESSAGE] = "Got http statuscode: %d" % (resp.status)
return result
except (asyncio.TimeoutError, aiohttp.ClientError) as err:
except (TimeoutError, aiohttp.ClientError) as err:
result[MESSAGE] = str(err)
return result
finally:

View File

@@ -181,7 +181,7 @@ async def _async_get_image(
that we can scale, however the majority of cases
are handled.
"""
with suppress(asyncio.CancelledError, asyncio.TimeoutError):
with suppress(asyncio.CancelledError, TimeoutError):
async with asyncio.timeout(timeout):
image_bytes = (
await _async_get_stream_image(
@@ -891,7 +891,7 @@ async def ws_camera_stream(
except HomeAssistantError as ex:
_LOGGER.error("Error requesting stream: %s", ex)
connection.send_error(msg["id"], "start_stream_failed", str(ex))
except asyncio.TimeoutError:
except TimeoutError:
_LOGGER.error("Timeout getting stream source")
connection.send_error(
msg["id"], "start_stream_failed", "Timeout getting stream source"
@@ -936,7 +936,7 @@ async def ws_camera_web_rtc_offer(
except (HomeAssistantError, ValueError) as ex:
_LOGGER.error("Error handling WebRTC offer: %s", ex)
connection.send_error(msg["id"], "web_rtc_offer_failed", str(ex))
except asyncio.TimeoutError:
except TimeoutError:
_LOGGER.error("Timeout handling WebRTC offer")
connection.send_error(
msg["id"], "web_rtc_offer_failed", "Timeout handling WebRTC offer"

View File

@@ -1,9 +1,7 @@
"""Consts for Cast integration."""
from __future__ import annotations
from typing import TYPE_CHECKING
from pychromecast.controllers.homeassistant import HomeAssistantController
from typing import TYPE_CHECKING, TypedDict
from homeassistant.helpers.dispatcher import SignalType
@@ -33,8 +31,17 @@ SIGNAL_CAST_REMOVED: SignalType[ChromecastInfo] = SignalType("cast_removed")
# Dispatcher signal fired when a Chromecast should show a Home Assistant Cast view.
SIGNAL_HASS_CAST_SHOW_VIEW: SignalType[
HomeAssistantController, str, str, str | None
HomeAssistantControllerData, str, str, str | None
] = SignalType("cast_show_view")
CONF_IGNORE_CEC = "ignore_cec"
CONF_KNOWN_HOSTS = "known_hosts"
class HomeAssistantControllerData(TypedDict):
"""Data for creating a HomeAssistantController."""
hass_url: str
hass_uuid: str
client_id: str | None
refresh_token: str

View File

@@ -1,7 +1,6 @@
"""Helpers to deal with Cast devices."""
from __future__ import annotations
import asyncio
import configparser
from dataclasses import dataclass
import logging
@@ -183,10 +182,10 @@ class CastStatusListener(
if self._valid:
self._cast_device.new_media_status(status)
def load_media_failed(self, item, error_code):
def load_media_failed(self, queue_item_id, error_code):
"""Handle reception of a new MediaStatus."""
if self._valid:
self._cast_device.load_media_failed(item, error_code)
self._cast_device.load_media_failed(queue_item_id, error_code)
def new_connection_status(self, status):
"""Handle reception of a new ConnectionStatus."""
@@ -257,7 +256,7 @@ async def _fetch_playlist(hass, url, supported_content_types):
playlist_data = (await resp.content.read(64 * 1024)).decode(charset)
except ValueError as err:
raise PlaylistError(f"Could not decode playlist {url}") from err
except asyncio.TimeoutError as err:
except TimeoutError as err:
raise PlaylistError(f"Timeout while fetching playlist {url}") from err
except aiohttp.client_exceptions.ClientError as err:
raise PlaylistError(f"Error while fetching playlist {url}") from err

View File

@@ -1,7 +1,6 @@
"""Home Assistant Cast integration for Cast."""
from __future__ import annotations
from pychromecast.controllers.homeassistant import HomeAssistantController
import voluptuous as vol
from homeassistant import auth, config_entries, core
@@ -11,7 +10,7 @@ from homeassistant.helpers import config_validation as cv, dispatcher, instance_
from homeassistant.helpers.network import NoURLAvailableError, get_url
from homeassistant.helpers.service import async_register_admin_service
from .const import DOMAIN, SIGNAL_HASS_CAST_SHOW_VIEW
from .const import DOMAIN, SIGNAL_HASS_CAST_SHOW_VIEW, HomeAssistantControllerData
SERVICE_SHOW_VIEW = "show_lovelace_view"
ATTR_VIEW_PATH = "view_path"
@@ -55,7 +54,7 @@ async def async_setup_ha_cast(
hass_uuid = await instance_id.async_get(hass)
controller = HomeAssistantController(
controller_data = HomeAssistantControllerData(
# If you are developing Home Assistant Cast, uncomment and set to
# your dev app id.
# app_id="5FE44367",
@@ -68,7 +67,7 @@ async def async_setup_ha_cast(
dispatcher.async_dispatcher_send(
hass,
SIGNAL_HASS_CAST_SHOW_VIEW,
controller,
controller_data,
call.data[ATTR_ENTITY_ID],
call.data[ATTR_VIEW_PATH],
call.data.get(ATTR_URL_PATH),

View File

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

View File

@@ -61,6 +61,7 @@ from .const import (
SIGNAL_CAST_DISCOVERED,
SIGNAL_CAST_REMOVED,
SIGNAL_HASS_CAST_SHOW_VIEW,
HomeAssistantControllerData,
)
from .discovery import setup_internal_discovery
from .helpers import (
@@ -389,15 +390,15 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
self.media_status_received = dt_util.utcnow()
self.schedule_update_ha_state()
def load_media_failed(self, item, error_code):
def load_media_failed(self, queue_item_id, error_code):
"""Handle load media failed."""
_LOGGER.debug(
"[%s %s] Load media failed with code %s(%s) for item %s",
"[%s %s] Load media failed with code %s(%s) for queue_item_id %s",
self.entity_id,
self._cast_info.friendly_name,
error_code,
MEDIA_PLAYER_ERROR_CODES.get(error_code, "unknown code"),
item,
queue_item_id,
)
def new_connection_status(self, connection_status):
@@ -951,7 +952,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
def _handle_signal_show_view(
self,
controller: HomeAssistantController,
controller_data: HomeAssistantControllerData,
entity_id: str,
view_path: str,
url_path: str | None,
@@ -961,6 +962,23 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity):
return
if self._hass_cast_controller is None:
def unregister() -> None:
"""Handle request to unregister the handler."""
if not self._hass_cast_controller or not self._chromecast:
return
_LOGGER.debug(
"[%s %s] Unregistering HomeAssistantController",
self.entity_id,
self._cast_info.friendly_name,
)
self._chromecast.unregister_handler(self._hass_cast_controller)
self._hass_cast_controller = None
controller = HomeAssistantController(
**controller_data, unregister=unregister
)
self._hass_cast_controller = controller
self._chromecast.register_handler(controller)

View File

@@ -64,8 +64,11 @@ class CCM15Climate(CoordinatorEntity[CCM15Coordinator], ClimateEntity):
ClimateEntityFeature.TARGET_TEMPERATURE
| ClimateEntityFeature.FAN_MODE
| ClimateEntityFeature.SWING_MODE
| ClimateEntityFeature.TURN_OFF
| ClimateEntityFeature.TURN_ON
)
_attr_name = None
_enable_turn_on_off_backwards_compatibility = False
def __init__(
self, ac_host: str, ac_index: int, coordinator: CCM15Coordinator

View File

@@ -55,7 +55,7 @@ async def get_cert_expiry_timestamp(
cert = await async_get_cert(hass, hostname, port)
except socket.gaierror as err:
raise ResolveFailed(f"Cannot resolve hostname: {hostname}") from err
except asyncio.TimeoutError as err:
except TimeoutError as err:
raise ConnectionTimeout(
f"Connection timeout with server: {hostname}:{port}"
) from err

View File

@@ -144,7 +144,7 @@ async def async_citybikes_request(hass, uri, schema):
json_response = await req.json()
return schema(json_response)
except (asyncio.TimeoutError, aiohttp.ClientError):
except (TimeoutError, aiohttp.ClientError):
_LOGGER.error("Could not connect to CityBikes API endpoint")
except ValueError:
_LOGGER.error("Received non-JSON data from CityBikes API endpoint")

View File

@@ -339,9 +339,6 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
def _report_turn_on_off(feature: str, method: str) -> None:
"""Log warning not implemented turn on/off feature."""
module = type(self).__module__
if module and "custom_components" not in module:
return
report_issue = self._suggest_report_issue()
if feature.startswith("TURN"):
message = (

View File

@@ -1,4 +1,5 @@
"""Intents for the client integration."""
from __future__ import annotations
import voluptuous as vol
@@ -36,24 +37,34 @@ class GetTemperatureIntent(intent.IntentHandler):
if not entities:
raise intent.IntentHandleError("No climate entities")
if "area" in slots:
# Filter by area
area_name = slots["area"]["value"]
name_slot = slots.get("name", {})
entity_name: str | None = name_slot.get("value")
entity_text: str | None = name_slot.get("text")
area_slot = slots.get("area", {})
area_id = area_slot.get("value")
if area_id:
# Filter by area and optionally name
area_name = area_slot.get("text")
for maybe_climate in intent.async_match_states(
hass, area_name=area_name, domains=[DOMAIN]
hass, name=entity_name, area_name=area_id, domains=[DOMAIN]
):
climate_state = maybe_climate
break
if climate_state is None:
raise intent.IntentHandleError(f"No climate entity in area {area_name}")
raise intent.NoStatesMatchedError(
name=entity_text or entity_name,
area=area_name or area_id,
domains={DOMAIN},
device_classes=None,
)
climate_entity = component.get_entity(climate_state.entity_id)
elif "name" in slots:
elif entity_name:
# Filter by name
entity_name = slots["name"]["value"]
for maybe_climate in intent.async_match_states(
hass, name=entity_name, domains=[DOMAIN]
):
@@ -61,7 +72,12 @@ class GetTemperatureIntent(intent.IntentHandler):
break
if climate_state is None:
raise intent.IntentHandleError(f"No climate entity named {entity_name}")
raise intent.NoStatesMatchedError(
name=entity_name,
area=None,
domains={DOMAIN},
device_classes=None,
)
climate_entity = component.get_entity(climate_state.entity_id)
else:

View File

@@ -1,7 +1,6 @@
"""Account linking via the cloud."""
from __future__ import annotations
import asyncio
from datetime import datetime
import logging
from typing import Any
@@ -69,7 +68,7 @@ async def _get_services(hass: HomeAssistant) -> list[dict[str, Any]]:
try:
services = await account_link.async_fetch_available_services(hass.data[DOMAIN])
except (aiohttp.ClientError, asyncio.TimeoutError):
except (aiohttp.ClientError, TimeoutError):
return []
hass.data[DATA_SERVICES] = services
@@ -114,7 +113,7 @@ class CloudOAuth2Implementation(config_entry_oauth2_flow.AbstractOAuth2Implement
try:
tokens = await helper.async_get_tokens()
except asyncio.TimeoutError:
except TimeoutError:
_LOGGER.info("Timeout fetching tokens for flow %s", flow_id)
except account_link.AccountLinkException as err:
_LOGGER.info(

View File

@@ -246,21 +246,27 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
await self._prefs.async_update(
alexa_settings_version=ALEXA_SETTINGS_VERSION
)
async_listen_entity_updates(
self.hass, CLOUD_ALEXA, self._async_exposed_entities_updated
self._on_deinitialize.append(
async_listen_entity_updates(
self.hass, CLOUD_ALEXA, self._async_exposed_entities_updated
)
)
async def on_hass_start(hass: HomeAssistant) -> None:
if self.enabled and ALEXA_DOMAIN not in self.hass.config.components:
await async_setup_component(self.hass, ALEXA_DOMAIN, {})
start.async_at_start(self.hass, on_hass_start)
start.async_at_started(self.hass, on_hass_started)
self._on_deinitialize.append(start.async_at_start(self.hass, on_hass_start))
self._on_deinitialize.append(start.async_at_started(self.hass, on_hass_started))
self._prefs.async_listen_updates(self._async_prefs_updated)
self.hass.bus.async_listen(
er.EVENT_ENTITY_REGISTRY_UPDATED,
self._handle_entity_registry_updated,
self._on_deinitialize.append(
self._prefs.async_listen_updates(self._async_prefs_updated)
)
self._on_deinitialize.append(
self.hass.bus.async_listen(
er.EVENT_ENTITY_REGISTRY_UPDATED,
self._handle_entity_registry_updated,
)
)
def _should_expose_legacy(self, entity_id: str) -> bool:
@@ -505,7 +511,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
return True
except asyncio.TimeoutError:
except TimeoutError:
_LOGGER.warning("Timeout trying to sync entities to Alexa")
return False

View File

@@ -9,7 +9,7 @@ from pathlib import Path
from typing import Any, Literal
import aiohttp
from hass_nabucasa.client import CloudClient as Interface
from hass_nabucasa.client import CloudClient as Interface, RemoteActivationNotAllowed
from homeassistant.components import google_assistant, persistent_notification, webhook
from homeassistant.components.alexa import (
@@ -213,6 +213,10 @@ class CloudClient(Interface):
"""Cleanup some stuff after logout."""
await self.prefs.async_set_username(None)
if self._alexa_config:
self._alexa_config.async_deinitialize()
self._alexa_config = None
if self._google_config:
self._google_config.async_deinitialize()
self._google_config = None
@@ -230,6 +234,8 @@ class CloudClient(Interface):
async def async_cloud_connect_update(self, connect: bool) -> None:
"""Process cloud remote message to client."""
if not self._prefs.remote_allow_remote_enable:
raise RemoteActivationNotAllowed
await self._prefs.async_update(remote_enabled=connect)
async def async_cloud_connection_info(
@@ -238,6 +244,7 @@ class CloudClient(Interface):
"""Process cloud connection info message to client."""
return {
"remote": {
"can_enable": self._prefs.remote_allow_remote_enable,
"connected": self.cloud.remote.is_connected,
"enabled": self._prefs.remote_enabled,
"instance_domain": self.cloud.remote.instance_domain,

View File

@@ -30,6 +30,8 @@ PREF_GOOGLE_DEFAULT_EXPOSE = "google_default_expose"
PREF_ALEXA_SETTINGS_VERSION = "alexa_settings_version"
PREF_GOOGLE_SETTINGS_VERSION = "google_settings_version"
PREF_TTS_DEFAULT_VOICE = "tts_default_voice"
PREF_GOOGLE_CONNECTED = "google_connected"
PREF_REMOTE_ALLOW_REMOTE_ENABLE = "remote_allow_remote_enable"
DEFAULT_TTS_DEFAULT_VOICE = ("en-US", "female")
DEFAULT_DISABLE_2FA = False
DEFAULT_ALEXA_REPORT_STATE = True

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