mirror of
https://github.com/home-assistant/core.git
synced 2025-08-07 06:35:10 +02:00
Merge branch 'dev' into heatpump
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
|
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@@ -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: |
|
||||
|
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -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"
|
||||
|
@@ -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:
|
||||
|
@@ -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.*
|
||||
|
@@ -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
|
||||
|
10
build.yaml
10
build.yaml
@@ -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
|
||||
|
@@ -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()
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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"
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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."""
|
||||
|
@@ -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__(
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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 = {
|
||||
|
@@ -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
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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."""
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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"
|
||||
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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(
|
||||
|
@@ -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]:
|
||||
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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": {
|
||||
|
@@ -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
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
)
|
||||
|
@@ -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
|
||||
|
@@ -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,
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
|
69
homeassistant/components/aprilaire/__init__.py
Normal file
69
homeassistant/components/aprilaire/__init__.py
Normal 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
|
302
homeassistant/components/aprilaire/climate.py
Normal file
302
homeassistant/components/aprilaire/climate.py
Normal 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()
|
72
homeassistant/components/aprilaire/config_flow.py
Normal file
72
homeassistant/components/aprilaire/config_flow.py
Normal 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"},
|
||||
)
|
11
homeassistant/components/aprilaire/const.py
Normal file
11
homeassistant/components/aprilaire/const.py
Normal 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"
|
209
homeassistant/components/aprilaire/coordinator.py
Normal file
209
homeassistant/components/aprilaire/coordinator.py
Normal 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
|
46
homeassistant/components/aprilaire/entity.py
Normal file
46
homeassistant/components/aprilaire/entity.py
Normal 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."""
|
11
homeassistant/components/aprilaire/manifest.json
Normal file
11
homeassistant/components/aprilaire/manifest.json
Normal 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"]
|
||||
}
|
28
homeassistant/components/aprilaire/strings.json
Normal file
28
homeassistant/components/aprilaire/strings.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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")
|
||||
|
@@ -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
|
||||
|
@@ -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."""
|
||||
|
@@ -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,
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
||||
|
@@ -7,5 +7,5 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "calculated",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["securetar==2023.3.0"]
|
||||
"requirements": ["securetar==2024.2.0"]
|
||||
}
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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()
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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."""
|
||||
|
@@ -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():
|
||||
|
@@ -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()
|
||||
|
@@ -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()
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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(
|
||||
|
@@ -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}")
|
||||
|
@@ -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%]"
|
||||
},
|
||||
|
@@ -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
|
||||
|
@@ -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__()
|
||||
|
@@ -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:
|
||||
|
@@ -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
|
||||
)
|
||||
|
@@ -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
|
||||
|
||||
|
||||
|
@@ -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()
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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}'"
|
||||
|
@@ -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:
|
||||
|
@@ -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"
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
@@ -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
|
||||
|
105
homeassistant/components/brother/icons.json
Normal file
105
homeassistant/components/brother/icons.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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,
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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:
|
||||
|
@@ -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"
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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),
|
||||
|
@@ -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."]
|
||||
}
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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")
|
||||
|
@@ -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 = (
|
||||
|
@@ -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:
|
||||
|
@@ -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(
|
||||
|
@@ -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
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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
Reference in New Issue
Block a user