Compare commits
72 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 94516de724 | |||
| a2027fc78c | |||
| be5577c2f9 | |||
| 93dc08a05f | |||
| def2ace4ec | |||
| 4f0261d739 | |||
| 6103811de8 | |||
| fd904c65a7 | |||
| 04bf8482b2 | |||
| f5fd5e0457 | |||
| 0de89b42aa | |||
| e8914552b1 | |||
| bfd302109e | |||
| 796ad47dd0 | |||
| e9915463a9 | |||
| 59aecda8cf | |||
| 7d00ccbbbc | |||
| 55a911120c | |||
| 80abf90c87 | |||
| 8539591307 | |||
| 6234deeee1 | |||
| 81fabb1bfa | |||
| ff4e5859cf | |||
| f2e42eafc7 | |||
| 63f28ae2fe | |||
| 5b6c6141c5 | |||
| 396ef7a642 | |||
| 17f59a5665 | |||
| 10846dc97b | |||
| 17bb00727d | |||
| bc021dbbc6 | |||
| e3cb9c0844 | |||
| 050e2c9404 | |||
| 5ea447ba48 | |||
| a23b063922 | |||
| c269d57259 | |||
| d512f327c5 | |||
| 9bf8c5a54b | |||
| 725e2f16f5 | |||
| d98d0cdad0 | |||
| e2f4aa893f | |||
| 6b81fa89d3 | |||
| c886587915 | |||
| 059d3eed98 | |||
| f9ae2b4453 | |||
| 742c7ba23f | |||
| e7ae5c5c24 | |||
| ae4fc9504a | |||
| 2ef337ec2e | |||
| 723b7bd532 | |||
| 4fdb11b0d8 | |||
| fe2e6c37f4 | |||
| 4a75c55a8f | |||
| dfb59469cf | |||
| bdb2e1e2e9 | |||
| c4f6f1e3d8 | |||
| fb3eae54ea | |||
| d3f8fce788 | |||
| 44e58a8c87 | |||
| 3d3879b0db | |||
| a8b1eb34f3 | |||
| fd77058def | |||
| b147ca6c5b | |||
| 670c4cacfa | |||
| 1ed0a89303 | |||
| ab0597da7b | |||
| a3db6bc8fa | |||
| 9bfc8f6e27 | |||
| 6fddef2dc5 | |||
| ec08a85aa0 | |||
| de7af575c5 | |||
| d3831bae4e |
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/aemet",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aemet_opendata"],
|
||||
"requirements": ["AEMET-OpenData==0.5.3"]
|
||||
"requirements": ["AEMET-OpenData==0.5.4"]
|
||||
}
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airgradient",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["airgradient==0.7.1"],
|
||||
"requirements": ["airgradient==0.8.0"],
|
||||
"zeroconf": ["_airgradient._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ class AirZoneConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
try:
|
||||
await airzone.get_version()
|
||||
except AirzoneError as err:
|
||||
except (AirzoneError, TimeoutError) as err:
|
||||
raise AbortFlow("cannot_connect") from err
|
||||
|
||||
return await self.async_step_discovered_connection()
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/airzone_cloud",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aioairzone_cloud"],
|
||||
"requirements": ["aioairzone-cloud==0.6.1"]
|
||||
"requirements": ["aioairzone-cloud==0.6.2"]
|
||||
}
|
||||
|
||||
@@ -244,7 +244,6 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
self._status: Status | None = None
|
||||
self._inputs: list[Input] = []
|
||||
self._presets: list[Preset] = []
|
||||
self._is_online = False
|
||||
self._muted = False
|
||||
self._master: BluesoundPlayer | None = None
|
||||
self._is_master = False
|
||||
@@ -312,26 +311,33 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
|
||||
async def _start_poll_command(self):
|
||||
"""Loop which polls the status of the player."""
|
||||
try:
|
||||
while True:
|
||||
while True:
|
||||
try:
|
||||
await self.async_update_status()
|
||||
|
||||
except (TimeoutError, ClientError):
|
||||
_LOGGER.error("Node %s:%s is offline, retrying later", self.name, self.port)
|
||||
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
|
||||
self.start_polling()
|
||||
|
||||
except CancelledError:
|
||||
_LOGGER.debug("Stopping the polling of node %s:%s", self.name, self.port)
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected error in %s:%s", self.name, self.port)
|
||||
raise
|
||||
except (TimeoutError, ClientError):
|
||||
_LOGGER.error(
|
||||
"Node %s:%s is offline, retrying later", self.host, self.port
|
||||
)
|
||||
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
|
||||
except CancelledError:
|
||||
_LOGGER.debug(
|
||||
"Stopping the polling of node %s:%s", self.host, self.port
|
||||
)
|
||||
return
|
||||
except Exception:
|
||||
_LOGGER.exception(
|
||||
"Unexpected error in %s:%s, retrying later", self.host, self.port
|
||||
)
|
||||
await asyncio.sleep(NODE_OFFLINE_CHECK_TIMEOUT)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Start the polling task."""
|
||||
await super().async_added_to_hass()
|
||||
|
||||
self._polling_task = self.hass.async_create_task(self._start_poll_command())
|
||||
self._polling_task = self.hass.async_create_background_task(
|
||||
self._start_poll_command(),
|
||||
name=f"bluesound.polling_{self.host}:{self.port}",
|
||||
)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Stop the polling task."""
|
||||
@@ -345,7 +351,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update internal status of the entity."""
|
||||
if not self._is_online:
|
||||
if not self.available:
|
||||
return
|
||||
|
||||
with suppress(TimeoutError):
|
||||
@@ -362,7 +368,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
try:
|
||||
status = await self._player.status(etag=etag, poll_timeout=120, timeout=125)
|
||||
|
||||
self._is_online = True
|
||||
self._attr_available = True
|
||||
self._last_status_update = dt_util.utcnow()
|
||||
self._status = status
|
||||
|
||||
@@ -391,7 +397,7 @@ class BluesoundPlayer(MediaPlayerEntity):
|
||||
|
||||
self.async_write_ha_state()
|
||||
except (TimeoutError, ClientError):
|
||||
self._is_online = False
|
||||
self._attr_available = False
|
||||
self._last_status_update = None
|
||||
self._status = None
|
||||
self.async_write_ha_state()
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"requirements": [
|
||||
"bleak==0.22.2",
|
||||
"bleak-retry-connector==3.5.0",
|
||||
"bluetooth-adapters==0.19.3",
|
||||
"bluetooth-adapters==0.19.4",
|
||||
"bluetooth-auto-recovery==1.4.2",
|
||||
"bluetooth-data-tools==1.19.4",
|
||||
"dbus-fast==2.22.1",
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/chacon_dio",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["dio_chacon_api"],
|
||||
"requirements": ["dio-chacon-wifi-api==1.1.0"]
|
||||
"requirements": ["dio-chacon-wifi-api==1.2.0"]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ from __future__ import annotations
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from coinbase.wallet.client import Client
|
||||
from coinbase.rest import RESTClient
|
||||
from coinbase.rest.rest_base import HTTPError
|
||||
from coinbase.wallet.client import Client as LegacyClient
|
||||
from coinbase.wallet.error import AuthenticationError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -15,8 +17,23 @@ from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.util import Throttle
|
||||
|
||||
from .const import (
|
||||
ACCOUNT_IS_VAULT,
|
||||
API_ACCOUNT_AMOUNT,
|
||||
API_ACCOUNT_AVALIABLE,
|
||||
API_ACCOUNT_BALANCE,
|
||||
API_ACCOUNT_CURRENCY,
|
||||
API_ACCOUNT_CURRENCY_CODE,
|
||||
API_ACCOUNT_HOLD,
|
||||
API_ACCOUNT_ID,
|
||||
API_ACCOUNTS_DATA,
|
||||
API_ACCOUNT_NAME,
|
||||
API_ACCOUNT_VALUE,
|
||||
API_ACCOUNTS,
|
||||
API_DATA,
|
||||
API_RATES_CURRENCY,
|
||||
API_RESOURCE_TYPE,
|
||||
API_TYPE_VAULT,
|
||||
API_V3_ACCOUNT_ID,
|
||||
API_V3_TYPE_VAULT,
|
||||
CONF_CURRENCIES,
|
||||
CONF_EXCHANGE_BASE,
|
||||
CONF_EXCHANGE_RATES,
|
||||
@@ -59,9 +76,16 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
def create_and_update_instance(entry: ConfigEntry) -> CoinbaseData:
|
||||
"""Create and update a Coinbase Data instance."""
|
||||
client = Client(entry.data[CONF_API_KEY], entry.data[CONF_API_TOKEN])
|
||||
if "organizations" not in entry.data[CONF_API_KEY]:
|
||||
client = LegacyClient(entry.data[CONF_API_KEY], entry.data[CONF_API_TOKEN])
|
||||
version = "v2"
|
||||
else:
|
||||
client = RESTClient(
|
||||
api_key=entry.data[CONF_API_KEY], api_secret=entry.data[CONF_API_TOKEN]
|
||||
)
|
||||
version = "v3"
|
||||
base_rate = entry.options.get(CONF_EXCHANGE_BASE, "USD")
|
||||
instance = CoinbaseData(client, base_rate)
|
||||
instance = CoinbaseData(client, base_rate, version)
|
||||
instance.update()
|
||||
return instance
|
||||
|
||||
@@ -86,42 +110,83 @@ async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> Non
|
||||
registry.async_remove(entity.entity_id)
|
||||
|
||||
|
||||
def get_accounts(client):
|
||||
def get_accounts(client, version):
|
||||
"""Handle paginated accounts."""
|
||||
response = client.get_accounts()
|
||||
accounts = response[API_ACCOUNTS_DATA]
|
||||
next_starting_after = response.pagination.next_starting_after
|
||||
|
||||
while next_starting_after:
|
||||
response = client.get_accounts(starting_after=next_starting_after)
|
||||
accounts += response[API_ACCOUNTS_DATA]
|
||||
if version == "v2":
|
||||
accounts = response[API_DATA]
|
||||
next_starting_after = response.pagination.next_starting_after
|
||||
|
||||
return accounts
|
||||
while next_starting_after:
|
||||
response = client.get_accounts(starting_after=next_starting_after)
|
||||
accounts += response[API_DATA]
|
||||
next_starting_after = response.pagination.next_starting_after
|
||||
|
||||
return [
|
||||
{
|
||||
API_ACCOUNT_ID: account[API_ACCOUNT_ID],
|
||||
API_ACCOUNT_NAME: account[API_ACCOUNT_NAME],
|
||||
API_ACCOUNT_CURRENCY: account[API_ACCOUNT_CURRENCY][
|
||||
API_ACCOUNT_CURRENCY_CODE
|
||||
],
|
||||
API_ACCOUNT_AMOUNT: account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT],
|
||||
ACCOUNT_IS_VAULT: account[API_RESOURCE_TYPE] == API_TYPE_VAULT,
|
||||
}
|
||||
for account in accounts
|
||||
]
|
||||
|
||||
accounts = response[API_ACCOUNTS]
|
||||
while response["has_next"]:
|
||||
response = client.get_accounts(cursor=response["cursor"])
|
||||
accounts += response["accounts"]
|
||||
|
||||
return [
|
||||
{
|
||||
API_ACCOUNT_ID: account[API_V3_ACCOUNT_ID],
|
||||
API_ACCOUNT_NAME: account[API_ACCOUNT_NAME],
|
||||
API_ACCOUNT_CURRENCY: account[API_ACCOUNT_CURRENCY],
|
||||
API_ACCOUNT_AMOUNT: account[API_ACCOUNT_AVALIABLE][API_ACCOUNT_VALUE]
|
||||
+ account[API_ACCOUNT_HOLD][API_ACCOUNT_VALUE],
|
||||
ACCOUNT_IS_VAULT: account[API_RESOURCE_TYPE] == API_V3_TYPE_VAULT,
|
||||
}
|
||||
for account in accounts
|
||||
]
|
||||
|
||||
|
||||
class CoinbaseData:
|
||||
"""Get the latest data and update the states."""
|
||||
|
||||
def __init__(self, client, exchange_base):
|
||||
def __init__(self, client, exchange_base, version):
|
||||
"""Init the coinbase data object."""
|
||||
|
||||
self.client = client
|
||||
self.accounts = None
|
||||
self.exchange_base = exchange_base
|
||||
self.exchange_rates = None
|
||||
self.user_id = self.client.get_current_user()[API_ACCOUNT_ID]
|
||||
if version == "v2":
|
||||
self.user_id = self.client.get_current_user()[API_ACCOUNT_ID]
|
||||
else:
|
||||
self.user_id = (
|
||||
"v3_" + client.get_portfolios()["portfolios"][0][API_V3_ACCOUNT_ID]
|
||||
)
|
||||
self.api_version = version
|
||||
|
||||
@Throttle(MIN_TIME_BETWEEN_UPDATES)
|
||||
def update(self):
|
||||
"""Get the latest data from coinbase."""
|
||||
|
||||
try:
|
||||
self.accounts = get_accounts(self.client)
|
||||
self.exchange_rates = self.client.get_exchange_rates(
|
||||
currency=self.exchange_base
|
||||
)
|
||||
except AuthenticationError as coinbase_error:
|
||||
self.accounts = get_accounts(self.client, self.api_version)
|
||||
if self.api_version == "v2":
|
||||
self.exchange_rates = self.client.get_exchange_rates(
|
||||
currency=self.exchange_base
|
||||
)
|
||||
else:
|
||||
self.exchange_rates = self.client.get(
|
||||
"/v2/exchange-rates",
|
||||
params={API_RATES_CURRENCY: self.exchange_base},
|
||||
)[API_DATA]
|
||||
except (AuthenticationError, HTTPError) as coinbase_error:
|
||||
_LOGGER.error(
|
||||
"Authentication error connecting to coinbase: %s", coinbase_error
|
||||
)
|
||||
|
||||
@@ -5,7 +5,9 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from coinbase.wallet.client import Client
|
||||
from coinbase.rest import RESTClient
|
||||
from coinbase.rest.rest_base import HTTPError
|
||||
from coinbase.wallet.client import Client as LegacyClient
|
||||
from coinbase.wallet.error import AuthenticationError
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -15,18 +17,17 @@ from homeassistant.config_entries import (
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN
|
||||
from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, CONF_API_VERSION
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
from . import get_accounts
|
||||
from .const import (
|
||||
ACCOUNT_IS_VAULT,
|
||||
API_ACCOUNT_CURRENCY,
|
||||
API_ACCOUNT_CURRENCY_CODE,
|
||||
API_DATA,
|
||||
API_RATES,
|
||||
API_RESOURCE_TYPE,
|
||||
API_TYPE_VAULT,
|
||||
CONF_CURRENCIES,
|
||||
CONF_EXCHANGE_BASE,
|
||||
CONF_EXCHANGE_PRECISION,
|
||||
@@ -49,8 +50,11 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
|
||||
def get_user_from_client(api_key, api_token):
|
||||
"""Get the user name from Coinbase API credentials."""
|
||||
client = Client(api_key, api_token)
|
||||
return client.get_current_user()
|
||||
if "organizations" not in api_key:
|
||||
client = LegacyClient(api_key, api_token)
|
||||
return client.get_current_user()["name"]
|
||||
client = RESTClient(api_key=api_key, api_secret=api_token)
|
||||
return client.get_portfolios()["portfolios"][0]["name"]
|
||||
|
||||
|
||||
async def validate_api(hass: HomeAssistant, data):
|
||||
@@ -60,11 +64,13 @@ async def validate_api(hass: HomeAssistant, data):
|
||||
user = await hass.async_add_executor_job(
|
||||
get_user_from_client, data[CONF_API_KEY], data[CONF_API_TOKEN]
|
||||
)
|
||||
except AuthenticationError as error:
|
||||
if "api key" in str(error):
|
||||
except (AuthenticationError, HTTPError) as error:
|
||||
if "api key" in str(error) or " 401 Client Error" in str(error):
|
||||
_LOGGER.debug("Coinbase rejected API credentials due to an invalid API key")
|
||||
raise InvalidKey from error
|
||||
if "invalid signature" in str(error):
|
||||
if "invalid signature" in str(
|
||||
error
|
||||
) or "'Could not deserialize key data" in str(error):
|
||||
_LOGGER.debug(
|
||||
"Coinbase rejected API credentials due to an invalid API secret"
|
||||
)
|
||||
@@ -73,8 +79,8 @@ async def validate_api(hass: HomeAssistant, data):
|
||||
raise InvalidAuth from error
|
||||
except ConnectionError as error:
|
||||
raise CannotConnect from error
|
||||
|
||||
return {"title": user["name"]}
|
||||
api_version = "v3" if "organizations" in data[CONF_API_KEY] else "v2"
|
||||
return {"title": user, "api_version": api_version}
|
||||
|
||||
|
||||
async def validate_options(hass: HomeAssistant, config_entry: ConfigEntry, options):
|
||||
@@ -82,14 +88,20 @@ async def validate_options(hass: HomeAssistant, config_entry: ConfigEntry, optio
|
||||
|
||||
client = hass.data[DOMAIN][config_entry.entry_id].client
|
||||
|
||||
accounts = await hass.async_add_executor_job(get_accounts, client)
|
||||
accounts = await hass.async_add_executor_job(
|
||||
get_accounts, client, config_entry.data.get("api_version", "v2")
|
||||
)
|
||||
|
||||
accounts_currencies = [
|
||||
account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE]
|
||||
account[API_ACCOUNT_CURRENCY]
|
||||
for account in accounts
|
||||
if account[API_RESOURCE_TYPE] != API_TYPE_VAULT
|
||||
if not account[ACCOUNT_IS_VAULT]
|
||||
]
|
||||
available_rates = await hass.async_add_executor_job(client.get_exchange_rates)
|
||||
if config_entry.data.get("api_version", "v2") == "v2":
|
||||
available_rates = await hass.async_add_executor_job(client.get_exchange_rates)
|
||||
else:
|
||||
resp = await hass.async_add_executor_job(client.get, "/v2/exchange-rates")
|
||||
available_rates = resp[API_DATA]
|
||||
if CONF_CURRENCIES in options:
|
||||
for currency in options[CONF_CURRENCIES]:
|
||||
if currency not in accounts_currencies:
|
||||
@@ -134,6 +146,7 @@ class CoinbaseConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
user_input[CONF_API_VERSION] = info["api_version"]
|
||||
return self.async_create_entry(title=info["title"], data=user_input)
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"""Constants used for Coinbase."""
|
||||
|
||||
ACCOUNT_IS_VAULT = "is_vault"
|
||||
|
||||
CONF_CURRENCIES = "account_balance_currencies"
|
||||
CONF_EXCHANGE_BASE = "exchange_base"
|
||||
CONF_EXCHANGE_RATES = "exchange_rate_currencies"
|
||||
@@ -10,18 +12,25 @@ DOMAIN = "coinbase"
|
||||
|
||||
# Constants for data returned by Coinbase API
|
||||
API_ACCOUNT_AMOUNT = "amount"
|
||||
API_ACCOUNT_AVALIABLE = "available_balance"
|
||||
API_ACCOUNT_BALANCE = "balance"
|
||||
API_ACCOUNT_CURRENCY = "currency"
|
||||
API_ACCOUNT_CURRENCY_CODE = "code"
|
||||
API_ACCOUNT_HOLD = "hold"
|
||||
API_ACCOUNT_ID = "id"
|
||||
API_ACCOUNT_NATIVE_BALANCE = "balance"
|
||||
API_ACCOUNT_NAME = "name"
|
||||
API_ACCOUNTS_DATA = "data"
|
||||
API_ACCOUNT_VALUE = "value"
|
||||
API_ACCOUNTS = "accounts"
|
||||
API_DATA = "data"
|
||||
API_RATES = "rates"
|
||||
API_RATES_CURRENCY = "currency"
|
||||
API_RESOURCE_PATH = "resource_path"
|
||||
API_RESOURCE_TYPE = "type"
|
||||
API_TYPE_VAULT = "vault"
|
||||
API_USD = "USD"
|
||||
API_V3_ACCOUNT_ID = "uuid"
|
||||
API_V3_TYPE_VAULT = "ACCOUNT_TYPE_VAULT"
|
||||
|
||||
WALLETS = {
|
||||
"1INCH": "1INCH",
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/coinbase",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["coinbase"],
|
||||
"requirements": ["coinbase==2.1.0"]
|
||||
"requirements": ["coinbase==2.1.0", "coinbase-advanced-py==1.2.2"]
|
||||
}
|
||||
|
||||
@@ -12,15 +12,12 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from . import CoinbaseData
|
||||
from .const import (
|
||||
ACCOUNT_IS_VAULT,
|
||||
API_ACCOUNT_AMOUNT,
|
||||
API_ACCOUNT_BALANCE,
|
||||
API_ACCOUNT_CURRENCY,
|
||||
API_ACCOUNT_CURRENCY_CODE,
|
||||
API_ACCOUNT_ID,
|
||||
API_ACCOUNT_NAME,
|
||||
API_RATES,
|
||||
API_RESOURCE_TYPE,
|
||||
API_TYPE_VAULT,
|
||||
CONF_CURRENCIES,
|
||||
CONF_EXCHANGE_PRECISION,
|
||||
CONF_EXCHANGE_PRECISION_DEFAULT,
|
||||
@@ -31,6 +28,7 @@ from .const import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_NATIVE_BALANCE = "Balance in native currency"
|
||||
ATTR_API_VERSION = "API Version"
|
||||
|
||||
CURRENCY_ICONS = {
|
||||
"BTC": "mdi:currency-btc",
|
||||
@@ -56,9 +54,9 @@ async def async_setup_entry(
|
||||
entities: list[SensorEntity] = []
|
||||
|
||||
provided_currencies: list[str] = [
|
||||
account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE]
|
||||
account[API_ACCOUNT_CURRENCY]
|
||||
for account in instance.accounts
|
||||
if account[API_RESOURCE_TYPE] != API_TYPE_VAULT
|
||||
if not account[ACCOUNT_IS_VAULT]
|
||||
]
|
||||
|
||||
desired_currencies: list[str] = []
|
||||
@@ -73,6 +71,11 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
for currency in desired_currencies:
|
||||
_LOGGER.debug(
|
||||
"Attempting to set up %s account sensor with %s API",
|
||||
currency,
|
||||
instance.api_version,
|
||||
)
|
||||
if currency not in provided_currencies:
|
||||
_LOGGER.warning(
|
||||
(
|
||||
@@ -85,12 +88,17 @@ async def async_setup_entry(
|
||||
entities.append(AccountSensor(instance, currency))
|
||||
|
||||
if CONF_EXCHANGE_RATES in config_entry.options:
|
||||
entities.extend(
|
||||
ExchangeRateSensor(
|
||||
instance, rate, exchange_base_currency, exchange_precision
|
||||
for rate in config_entry.options[CONF_EXCHANGE_RATES]:
|
||||
_LOGGER.debug(
|
||||
"Attempting to set up %s account sensor with %s API",
|
||||
rate,
|
||||
instance.api_version,
|
||||
)
|
||||
entities.append(
|
||||
ExchangeRateSensor(
|
||||
instance, rate, exchange_base_currency, exchange_precision
|
||||
)
|
||||
)
|
||||
for rate in config_entry.options[CONF_EXCHANGE_RATES]
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
|
||||
@@ -105,26 +113,21 @@ class AccountSensor(SensorEntity):
|
||||
self._coinbase_data = coinbase_data
|
||||
self._currency = currency
|
||||
for account in coinbase_data.accounts:
|
||||
if (
|
||||
account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE] != currency
|
||||
or account[API_RESOURCE_TYPE] == API_TYPE_VAULT
|
||||
):
|
||||
if account[API_ACCOUNT_CURRENCY] != currency or account[ACCOUNT_IS_VAULT]:
|
||||
continue
|
||||
self._attr_name = f"Coinbase {account[API_ACCOUNT_NAME]}"
|
||||
self._attr_unique_id = (
|
||||
f"coinbase-{account[API_ACCOUNT_ID]}-wallet-"
|
||||
f"{account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE]}"
|
||||
f"{account[API_ACCOUNT_CURRENCY]}"
|
||||
)
|
||||
self._attr_native_value = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT]
|
||||
self._attr_native_unit_of_measurement = account[API_ACCOUNT_CURRENCY][
|
||||
API_ACCOUNT_CURRENCY_CODE
|
||||
]
|
||||
self._attr_native_value = account[API_ACCOUNT_AMOUNT]
|
||||
self._attr_native_unit_of_measurement = account[API_ACCOUNT_CURRENCY]
|
||||
self._attr_icon = CURRENCY_ICONS.get(
|
||||
account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE],
|
||||
account[API_ACCOUNT_CURRENCY],
|
||||
DEFAULT_COIN_ICON,
|
||||
)
|
||||
self._native_balance = round(
|
||||
float(account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT])
|
||||
float(account[API_ACCOUNT_AMOUNT])
|
||||
/ float(coinbase_data.exchange_rates[API_RATES][currency]),
|
||||
2,
|
||||
)
|
||||
@@ -144,21 +147,26 @@ class AccountSensor(SensorEntity):
|
||||
"""Return the state attributes of the sensor."""
|
||||
return {
|
||||
ATTR_NATIVE_BALANCE: f"{self._native_balance} {self._coinbase_data.exchange_base}",
|
||||
ATTR_API_VERSION: self._coinbase_data.api_version,
|
||||
}
|
||||
|
||||
def update(self) -> None:
|
||||
"""Get the latest state of the sensor."""
|
||||
_LOGGER.debug(
|
||||
"Updating %s account sensor with %s API",
|
||||
self._currency,
|
||||
self._coinbase_data.api_version,
|
||||
)
|
||||
self._coinbase_data.update()
|
||||
for account in self._coinbase_data.accounts:
|
||||
if (
|
||||
account[API_ACCOUNT_CURRENCY][API_ACCOUNT_CURRENCY_CODE]
|
||||
!= self._currency
|
||||
or account[API_RESOURCE_TYPE] == API_TYPE_VAULT
|
||||
account[API_ACCOUNT_CURRENCY] != self._currency
|
||||
or account[ACCOUNT_IS_VAULT]
|
||||
):
|
||||
continue
|
||||
self._attr_native_value = account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT]
|
||||
self._attr_native_value = account[API_ACCOUNT_AMOUNT]
|
||||
self._native_balance = round(
|
||||
float(account[API_ACCOUNT_BALANCE][API_ACCOUNT_AMOUNT])
|
||||
float(account[API_ACCOUNT_AMOUNT])
|
||||
/ float(self._coinbase_data.exchange_rates[API_RATES][self._currency]),
|
||||
2,
|
||||
)
|
||||
@@ -202,8 +210,13 @@ class ExchangeRateSensor(SensorEntity):
|
||||
|
||||
def update(self) -> None:
|
||||
"""Get the latest state of the sensor."""
|
||||
_LOGGER.debug(
|
||||
"Updating %s rate sensor with %s API",
|
||||
self._currency,
|
||||
self._coinbase_data.api_version,
|
||||
)
|
||||
self._coinbase_data.update()
|
||||
self._attr_native_value = round(
|
||||
1 / float(self._coinbase_data.exchange_rates.rates[self._currency]),
|
||||
1 / float(self._coinbase_data.exchange_rates[API_RATES][self._currency]),
|
||||
self._precision,
|
||||
)
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
"""Support for Concord232 alarm control panels."""
|
||||
|
||||
# mypy: ignore-errors
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
# from concord232 import client as concord232_client
|
||||
from concord232 import client as concord232_client
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
"""Support for exposing Concord232 elements as sensors."""
|
||||
|
||||
# mypy: ignore-errors
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
# from concord232 import client as concord232_client
|
||||
from concord232 import client as concord232_client
|
||||
import requests
|
||||
import voluptuous as vol
|
||||
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
"domain": "concord232",
|
||||
"name": "Concord232",
|
||||
"codeowners": [],
|
||||
"disabled": "This integration is disabled because it uses non-open source code to operate.",
|
||||
"documentation": "https://www.home-assistant.io/integrations/concord232",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["concord232", "stevedore"],
|
||||
"requirements": ["concord232==0.15"]
|
||||
"requirements": ["concord232==0.15.1"]
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
extend = "../../../pyproject.toml"
|
||||
|
||||
lint.extend-ignore = [
|
||||
"F821"
|
||||
]
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/daikin",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pydaikin"],
|
||||
"requirements": ["pydaikin==2.13.1"],
|
||||
"requirements": ["pydaikin==2.13.4"],
|
||||
"zeroconf": ["_dkapi._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ class ConfiguredDoorBird:
|
||||
title: str | None = data.get("title")
|
||||
if not title or not title.startswith("Home Assistant"):
|
||||
continue
|
||||
event = title.split("(")[1].strip(")")
|
||||
event = title.partition("(")[2].strip(")")
|
||||
if input_type := favorite_input_type.get(identifier):
|
||||
events.append(DoorbirdEvent(event, input_type))
|
||||
elif input_type := default_event_types.get(event):
|
||||
|
||||
@@ -431,41 +431,42 @@ def rename_old_gas_to_mbus(
|
||||
) -> None:
|
||||
"""Rename old gas sensor to mbus variant."""
|
||||
dev_reg = dr.async_get(hass)
|
||||
device_entry_v1 = dev_reg.async_get_device(identifiers={(DOMAIN, entry.entry_id)})
|
||||
if device_entry_v1 is not None:
|
||||
device_id = device_entry_v1.id
|
||||
for dev_id in (mbus_device_id, entry.entry_id):
|
||||
device_entry_v1 = dev_reg.async_get_device(identifiers={(DOMAIN, dev_id)})
|
||||
if device_entry_v1 is not None:
|
||||
device_id = device_entry_v1.id
|
||||
|
||||
ent_reg = er.async_get(hass)
|
||||
entries = er.async_entries_for_device(ent_reg, device_id)
|
||||
ent_reg = er.async_get(hass)
|
||||
entries = er.async_entries_for_device(ent_reg, device_id)
|
||||
|
||||
for entity in entries:
|
||||
if entity.unique_id.endswith(
|
||||
"belgium_5min_gas_meter_reading"
|
||||
) or entity.unique_id.endswith("hourly_gas_meter_reading"):
|
||||
try:
|
||||
ent_reg.async_update_entity(
|
||||
entity.entity_id,
|
||||
new_unique_id=mbus_device_id,
|
||||
device_id=mbus_device_id,
|
||||
)
|
||||
except ValueError:
|
||||
LOGGER.debug(
|
||||
"Skip migration of %s because it already exists",
|
||||
entity.entity_id,
|
||||
)
|
||||
else:
|
||||
LOGGER.debug(
|
||||
"Migrated entity %s from unique id %s to %s",
|
||||
entity.entity_id,
|
||||
entity.unique_id,
|
||||
mbus_device_id,
|
||||
)
|
||||
# Cleanup old device
|
||||
dev_entities = er.async_entries_for_device(
|
||||
ent_reg, device_id, include_disabled_entities=True
|
||||
)
|
||||
if not dev_entities:
|
||||
dev_reg.async_remove_device(device_id)
|
||||
for entity in entries:
|
||||
if entity.unique_id.endswith(
|
||||
"belgium_5min_gas_meter_reading"
|
||||
) or entity.unique_id.endswith("hourly_gas_meter_reading"):
|
||||
try:
|
||||
ent_reg.async_update_entity(
|
||||
entity.entity_id,
|
||||
new_unique_id=mbus_device_id,
|
||||
device_id=mbus_device_id,
|
||||
)
|
||||
except ValueError:
|
||||
LOGGER.debug(
|
||||
"Skip migration of %s because it already exists",
|
||||
entity.entity_id,
|
||||
)
|
||||
else:
|
||||
LOGGER.debug(
|
||||
"Migrated entity %s from unique id %s to %s",
|
||||
entity.entity_id,
|
||||
entity.unique_id,
|
||||
mbus_device_id,
|
||||
)
|
||||
# Cleanup old device
|
||||
dev_entities = er.async_entries_for_device(
|
||||
ent_reg, device_id, include_disabled_entities=True
|
||||
)
|
||||
if not dev_entities:
|
||||
dev_reg.async_remove_device(device_id)
|
||||
|
||||
|
||||
def is_supported_description(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
import logging
|
||||
import ssl
|
||||
from typing import Any, cast
|
||||
@@ -105,11 +106,14 @@ async def _validate_input(
|
||||
if not user_input.get(CONF_VERIFY_MQTT_CERTIFICATE, True) and mqtt_url:
|
||||
ssl_context = get_default_no_verify_context()
|
||||
|
||||
mqtt_config = create_mqtt_config(
|
||||
device_id=device_id,
|
||||
country=country,
|
||||
override_mqtt_url=mqtt_url,
|
||||
ssl_context=ssl_context,
|
||||
mqtt_config = await hass.async_add_executor_job(
|
||||
partial(
|
||||
create_mqtt_config,
|
||||
device_id=device_id,
|
||||
country=country,
|
||||
override_mqtt_url=mqtt_url,
|
||||
ssl_context=ssl_context,
|
||||
)
|
||||
)
|
||||
|
||||
client = MqttClient(mqtt_config, authenticator)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from functools import partial
|
||||
import logging
|
||||
import ssl
|
||||
from typing import Any
|
||||
@@ -64,32 +65,28 @@ class EcovacsController:
|
||||
if not config.get(CONF_VERIFY_MQTT_CERTIFICATE, True) and mqtt_url:
|
||||
ssl_context = get_default_no_verify_context()
|
||||
|
||||
self._mqtt = MqttClient(
|
||||
create_mqtt_config(
|
||||
device_id=self._device_id,
|
||||
country=country,
|
||||
override_mqtt_url=mqtt_url,
|
||||
ssl_context=ssl_context,
|
||||
),
|
||||
self._authenticator,
|
||||
self._mqtt_config_fn = partial(
|
||||
create_mqtt_config,
|
||||
device_id=self._device_id,
|
||||
country=country,
|
||||
override_mqtt_url=mqtt_url,
|
||||
ssl_context=ssl_context,
|
||||
)
|
||||
self._mqtt_client: MqttClient | None = None
|
||||
|
||||
self._added_legacy_entities: set[str] = set()
|
||||
|
||||
async def initialize(self) -> None:
|
||||
"""Init controller."""
|
||||
mqtt_config_verfied = False
|
||||
try:
|
||||
devices = await self._api_client.get_devices()
|
||||
credentials = await self._authenticator.authenticate()
|
||||
for device_config in devices:
|
||||
if isinstance(device_config, DeviceInfo):
|
||||
# MQTT device
|
||||
if not mqtt_config_verfied:
|
||||
await self._mqtt.verify_config()
|
||||
mqtt_config_verfied = True
|
||||
device = Device(device_config, self._authenticator)
|
||||
await device.initialize(self._mqtt)
|
||||
mqtt = await self._get_mqtt_client()
|
||||
await device.initialize(mqtt)
|
||||
self._devices.append(device)
|
||||
else:
|
||||
# Legacy device
|
||||
@@ -116,7 +113,8 @@ class EcovacsController:
|
||||
await device.teardown()
|
||||
for legacy_device in self._legacy_devices:
|
||||
await self._hass.async_add_executor_job(legacy_device.disconnect)
|
||||
await self._mqtt.disconnect()
|
||||
if self._mqtt_client is not None:
|
||||
await self._mqtt_client.disconnect()
|
||||
await self._authenticator.teardown()
|
||||
|
||||
def add_legacy_entity(self, device: VacBot, component: str) -> None:
|
||||
@@ -127,6 +125,16 @@ class EcovacsController:
|
||||
"""Check if legacy entity is added."""
|
||||
return f"{device.vacuum['did']}_{component}" in self._added_legacy_entities
|
||||
|
||||
async def _get_mqtt_client(self) -> MqttClient:
|
||||
"""Return validated MQTT client."""
|
||||
if self._mqtt_client is None:
|
||||
config = await self._hass.async_add_executor_job(self._mqtt_config_fn)
|
||||
mqtt = MqttClient(config, self._authenticator)
|
||||
await mqtt.verify_config()
|
||||
self._mqtt_client = mqtt
|
||||
|
||||
return self._mqtt_client
|
||||
|
||||
@property
|
||||
def devices(self) -> list[Device]:
|
||||
"""Return devices."""
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["openwebif"],
|
||||
"requirements": ["openwebifpy==4.2.5"]
|
||||
"requirements": ["openwebifpy==4.2.7"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_CONDITION_CLEAR_NIGHT,
|
||||
ATTR_CONDITION_CLOUDY,
|
||||
@@ -190,10 +192,12 @@ def get_forecast(ec_data, hourly) -> list[Forecast] | None:
|
||||
if not (half_days := ec_data.daily_forecasts):
|
||||
return None
|
||||
|
||||
def get_day_forecast(fcst: list[dict[str, str]]) -> Forecast:
|
||||
def get_day_forecast(
|
||||
fcst: list[dict[str, Any]],
|
||||
) -> Forecast:
|
||||
high_temp = int(fcst[0]["temperature"]) if len(fcst) == 2 else None
|
||||
return {
|
||||
ATTR_FORECAST_TIME: fcst[0]["timestamp"],
|
||||
ATTR_FORECAST_TIME: fcst[0]["timestamp"].isoformat(),
|
||||
ATTR_FORECAST_NATIVE_TEMP: high_temp,
|
||||
ATTR_FORECAST_NATIVE_TEMP_LOW: int(fcst[-1]["temperature"]),
|
||||
ATTR_FORECAST_PRECIPITATION_PROBABILITY: int(
|
||||
|
||||
@@ -653,8 +653,6 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
entities: list[er.RegistryEntry] = er.async_entries_for_config_entry(
|
||||
entity_reg, config_entry.entry_id
|
||||
)
|
||||
|
||||
orphan_macs: set[str] = set()
|
||||
for entity in entities:
|
||||
entry_mac = entity.unique_id.split("_")[0]
|
||||
if (
|
||||
@@ -662,17 +660,16 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
or "_internet_access" in entity.unique_id
|
||||
) and entry_mac not in device_hosts:
|
||||
_LOGGER.info("Removing orphan entity entry %s", entity.entity_id)
|
||||
orphan_macs.add(entry_mac)
|
||||
entity_reg.async_remove(entity.entity_id)
|
||||
|
||||
device_reg = dr.async_get(self.hass)
|
||||
orphan_connections = {
|
||||
(CONNECTION_NETWORK_MAC, dr.format_mac(mac)) for mac in orphan_macs
|
||||
valid_connections = {
|
||||
(CONNECTION_NETWORK_MAC, dr.format_mac(mac)) for mac in device_hosts
|
||||
}
|
||||
for device in dr.async_entries_for_config_entry(
|
||||
device_reg, config_entry.entry_id
|
||||
):
|
||||
if any(con in device.connections for con in orphan_connections):
|
||||
if not any(con in device.connections for con in valid_connections):
|
||||
_LOGGER.debug("Removing obsolete device entry %s", device.name)
|
||||
device_reg.async_update_device(
|
||||
device.id, remove_config_entry_id=config_entry.entry_id
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20240806.1"]
|
||||
"requirements": ["home-assistant-frontend==20240809.0"]
|
||||
}
|
||||
|
||||
@@ -45,15 +45,13 @@ class GlancesDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
except exceptions.GlancesApiError as err:
|
||||
raise UpdateFailed from err
|
||||
# Update computed values
|
||||
uptime: datetime | None = self.data["computed"]["uptime"] if self.data else None
|
||||
uptime: datetime | None = None
|
||||
up_duration: timedelta | None = None
|
||||
if up_duration := parse_duration(data.get("uptime")):
|
||||
if "uptime" in data and (up_duration := parse_duration(data["uptime"])):
|
||||
uptime = self.data["computed"]["uptime"] if self.data else None
|
||||
# Update uptime if previous value is None or previous uptime is bigger than
|
||||
# new uptime (i.e. server restarted)
|
||||
if (
|
||||
self.data is None
|
||||
or self.data["computed"]["uptime_duration"] > up_duration
|
||||
):
|
||||
if uptime is None or self.data["computed"]["uptime_duration"] > up_duration:
|
||||
uptime = utcnow() - up_duration
|
||||
data["computed"] = {"uptime_duration": up_duration, "uptime": uptime}
|
||||
return data or {}
|
||||
|
||||
@@ -325,6 +325,7 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit
|
||||
|
||||
entity_description: GlancesSensorEntityDescription
|
||||
_attr_has_entity_name = True
|
||||
_data_valid: bool = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -351,14 +352,7 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Set sensor unavailable when native value is invalid."""
|
||||
if super().available:
|
||||
return (
|
||||
not self._numeric_state_expected
|
||||
or isinstance(value := self.native_value, (int, float))
|
||||
or isinstance(value, str)
|
||||
and value.isnumeric()
|
||||
)
|
||||
return False
|
||||
return super().available and self._data_valid
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
@@ -368,10 +362,19 @@ class GlancesSensor(CoordinatorEntity[GlancesDataUpdateCoordinator], SensorEntit
|
||||
|
||||
def _update_native_value(self) -> None:
|
||||
"""Update sensor native value from coordinator data."""
|
||||
data = self.coordinator.data[self.entity_description.type]
|
||||
if dict_val := data.get(self._sensor_label):
|
||||
data = self.coordinator.data.get(self.entity_description.type)
|
||||
if data and (dict_val := data.get(self._sensor_label)):
|
||||
self._attr_native_value = dict_val.get(self.entity_description.key)
|
||||
elif self.entity_description.key in data:
|
||||
elif data and (self.entity_description.key in data):
|
||||
self._attr_native_value = data.get(self.entity_description.key)
|
||||
else:
|
||||
self._attr_native_value = None
|
||||
self._update_data_valid()
|
||||
|
||||
def _update_data_valid(self) -> None:
|
||||
self._data_valid = self._attr_native_value is not None and (
|
||||
not self._numeric_state_expected
|
||||
or isinstance(self._attr_native_value, (int, float))
|
||||
or isinstance(self._attr_native_value, str)
|
||||
and self._attr_native_value.isnumeric()
|
||||
)
|
||||
|
||||
@@ -22,8 +22,9 @@ from homeassistant.components.notify import (
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_SERVICE,
|
||||
CONF_ACTION,
|
||||
CONF_ENTITIES,
|
||||
CONF_SERVICE,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
@@ -36,11 +37,37 @@ from .entity import GroupEntity
|
||||
|
||||
CONF_SERVICES = "services"
|
||||
|
||||
|
||||
def _backward_compat_schema(value: Any | None) -> Any:
|
||||
"""Backward compatibility for notify service schemas."""
|
||||
|
||||
if not isinstance(value, dict):
|
||||
return value
|
||||
|
||||
# `service` has been renamed to `action`
|
||||
if CONF_SERVICE in value:
|
||||
if CONF_ACTION in value:
|
||||
raise vol.Invalid(
|
||||
"Cannot specify both 'service' and 'action'. Please use 'action' only."
|
||||
)
|
||||
value[CONF_ACTION] = value.pop(CONF_SERVICE)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend(
|
||||
{
|
||||
vol.Required(CONF_SERVICES): vol.All(
|
||||
cv.ensure_list,
|
||||
[{vol.Required(ATTR_SERVICE): cv.slug, vol.Optional(ATTR_DATA): dict}],
|
||||
[
|
||||
vol.All(
|
||||
_backward_compat_schema,
|
||||
{
|
||||
vol.Required(CONF_ACTION): cv.slug,
|
||||
vol.Optional(ATTR_DATA): dict,
|
||||
},
|
||||
)
|
||||
],
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -88,7 +115,7 @@ class GroupNotifyPlatform(BaseNotificationService):
|
||||
tasks.append(
|
||||
asyncio.create_task(
|
||||
self.hass.services.async_call(
|
||||
DOMAIN, entity[ATTR_SERVICE], sending_payload, blocking=True
|
||||
DOMAIN, entity[CONF_ACTION], sending_payload, blocking=True
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -60,8 +60,11 @@
|
||||
"integration_not_found": {
|
||||
"title": "Integration {domain} not found",
|
||||
"fix_flow": {
|
||||
"abort": {
|
||||
"issue_ignored": "Not existing integration {domain} ignored."
|
||||
},
|
||||
"step": {
|
||||
"remove_entries": {
|
||||
"init": {
|
||||
"title": "[%key:component::homeassistant::issues::integration_not_found::title%]",
|
||||
"description": "The integration `{domain}` could not be found. This happens when a (custom) integration was removed from Home Assistant, but there are still configurations for this `integration`. Please use the buttons below to either remove the previous configurations for `{domain}` or ignore this.",
|
||||
"menu_options": {
|
||||
|
||||
@@ -22,6 +22,7 @@ from homeassistant.components import (
|
||||
sensor,
|
||||
)
|
||||
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
|
||||
from homeassistant.components.event import DOMAIN as EVENT_DOMAIN
|
||||
from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN
|
||||
from homeassistant.components.media_player import (
|
||||
DOMAIN as MEDIA_PLAYER_DOMAIN,
|
||||
@@ -167,9 +168,11 @@ CAMERA_SCHEMA = BASIC_INFO_SCHEMA.extend(
|
||||
vol.Optional(
|
||||
CONF_VIDEO_PACKET_SIZE, default=DEFAULT_VIDEO_PACKET_SIZE
|
||||
): cv.positive_int,
|
||||
vol.Optional(CONF_LINKED_MOTION_SENSOR): cv.entity_domain(binary_sensor.DOMAIN),
|
||||
vol.Optional(CONF_LINKED_MOTION_SENSOR): cv.entity_domain(
|
||||
[binary_sensor.DOMAIN, EVENT_DOMAIN]
|
||||
),
|
||||
vol.Optional(CONF_LINKED_DOORBELL_SENSOR): cv.entity_domain(
|
||||
binary_sensor.DOMAIN
|
||||
[binary_sensor.DOMAIN, EVENT_DOMAIN]
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -845,21 +845,41 @@ class HKDevice:
|
||||
|
||||
async def async_update(self, now: datetime | None = None) -> None:
|
||||
"""Poll state of all entities attached to this bridge/accessory."""
|
||||
to_poll = self.pollable_characteristics
|
||||
accessories = self.entity_map.accessories
|
||||
|
||||
if (
|
||||
len(self.entity_map.accessories) == 1
|
||||
len(accessories) == 1
|
||||
and self.available
|
||||
and not (self.pollable_characteristics - self.watchable_characteristics)
|
||||
and not (to_poll - self.watchable_characteristics)
|
||||
and self.pairing.is_available
|
||||
and await self.pairing.controller.async_reachable(
|
||||
self.unique_id, timeout=5.0
|
||||
)
|
||||
):
|
||||
# If its a single accessory and all chars are watchable,
|
||||
# we don't need to poll.
|
||||
_LOGGER.debug("Accessory is reachable, skip polling: %s", self.unique_id)
|
||||
return
|
||||
# only poll the firmware version to keep the connection alive
|
||||
# https://github.com/home-assistant/core/issues/123412
|
||||
#
|
||||
# Firmware revision is used here since iOS does this to keep camera
|
||||
# connections alive, and the goal is to not regress
|
||||
# https://github.com/home-assistant/core/issues/116143
|
||||
# by polling characteristics that are not normally polled frequently
|
||||
# and may not be tested by the device vendor.
|
||||
#
|
||||
_LOGGER.debug(
|
||||
"Accessory is reachable, limiting poll to firmware version: %s",
|
||||
self.unique_id,
|
||||
)
|
||||
first_accessory = accessories[0]
|
||||
accessory_info = first_accessory.services.first(
|
||||
service_type=ServicesTypes.ACCESSORY_INFORMATION
|
||||
)
|
||||
assert accessory_info is not None
|
||||
firmware_iid = accessory_info[CharacteristicsTypes.FIRMWARE_REVISION].iid
|
||||
to_poll = {(first_accessory.aid, firmware_iid)}
|
||||
|
||||
if not self.pollable_characteristics:
|
||||
if not to_poll:
|
||||
self.async_update_available_state()
|
||||
_LOGGER.debug(
|
||||
"HomeKit connection not polling any characteristics: %s", self.unique_id
|
||||
@@ -892,9 +912,7 @@ class HKDevice:
|
||||
_LOGGER.debug("Starting HomeKit device update: %s", self.unique_id)
|
||||
|
||||
try:
|
||||
new_values_dict = await self.get_characteristics(
|
||||
self.pollable_characteristics
|
||||
)
|
||||
new_values_dict = await self.get_characteristics(to_poll)
|
||||
except AccessoryNotFoundError:
|
||||
# Not only did the connection fail, but also the accessory is not
|
||||
# visible on the network.
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homekit_controller",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiohomekit", "commentjson"],
|
||||
"requirements": ["aiohomekit==3.2.1"],
|
||||
"requirements": ["aiohomekit==3.2.2"],
|
||||
"zeroconf": ["_hap._tcp.local.", "_hap._udp.local."]
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["homewizard_energy"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["python-homewizard-energy==v6.1.1"],
|
||||
"requirements": ["python-homewizard-energy==v6.2.0"],
|
||||
"zeroconf": ["_hwenergy._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/homeworks",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pyhomeworks"],
|
||||
"requirements": ["pyhomeworks==1.1.0"]
|
||||
"requirements": ["pyhomeworks==1.1.1"]
|
||||
}
|
||||
|
||||
@@ -533,7 +533,7 @@ class HTML5NotificationService(BaseNotificationService):
|
||||
elif response.status_code > 399:
|
||||
_LOGGER.error(
|
||||
"There was an issue sending the notification %s: %s",
|
||||
response.status,
|
||||
response.status_code,
|
||||
response.text,
|
||||
)
|
||||
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["jvcprojector"],
|
||||
"requirements": ["pyjvcprojector==1.0.11"]
|
||||
"requirements": ["pyjvcprojector==1.0.12"]
|
||||
}
|
||||
|
||||
@@ -147,18 +147,10 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Start the KNX integration."""
|
||||
hass.data[DATA_HASS_CONFIG] = config
|
||||
conf: ConfigType | None = config.get(DOMAIN)
|
||||
|
||||
if conf is None:
|
||||
# If we have a config entry, setup is done by that config entry.
|
||||
# If there is no config entry, this should fail.
|
||||
return bool(hass.config_entries.async_entries(DOMAIN))
|
||||
|
||||
conf = dict(conf)
|
||||
hass.data[DATA_KNX_CONFIG] = conf
|
||||
if (conf := config.get(DOMAIN)) is not None:
|
||||
hass.data[DATA_KNX_CONFIG] = dict(conf)
|
||||
|
||||
register_knx_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
from xknx import XKNX
|
||||
from xknx.devices import Climate as XknxClimate, ClimateMode as XknxClimateMode
|
||||
from xknx.devices import (
|
||||
Climate as XknxClimate,
|
||||
ClimateMode as XknxClimateMode,
|
||||
Device as XknxDevice,
|
||||
)
|
||||
from xknx.dpt.dpt_20 import HVACControllerMode
|
||||
|
||||
from homeassistant import config_entries
|
||||
@@ -241,12 +245,9 @@ class KNXClimate(KnxYamlEntity, ClimateEntity):
|
||||
if self._device.supports_on_off and not self._device.is_on:
|
||||
return HVACMode.OFF
|
||||
if self._device.mode is not None and self._device.mode.supports_controller_mode:
|
||||
hvac_mode = CONTROLLER_MODES.get(
|
||||
return CONTROLLER_MODES.get(
|
||||
self._device.mode.controller_mode, self.default_hvac_mode
|
||||
)
|
||||
if hvac_mode is not HVACMode.OFF:
|
||||
self._last_hvac_mode = hvac_mode
|
||||
return hvac_mode
|
||||
return self.default_hvac_mode
|
||||
|
||||
@property
|
||||
@@ -261,11 +262,15 @@ class KNXClimate(KnxYamlEntity, ClimateEntity):
|
||||
|
||||
if self._device.supports_on_off:
|
||||
if not ha_controller_modes:
|
||||
ha_controller_modes.append(self.default_hvac_mode)
|
||||
ha_controller_modes.append(self._last_hvac_mode)
|
||||
ha_controller_modes.append(HVACMode.OFF)
|
||||
|
||||
hvac_modes = list(set(filter(None, ha_controller_modes)))
|
||||
return hvac_modes if hvac_modes else [self.default_hvac_mode]
|
||||
return (
|
||||
hvac_modes
|
||||
if hvac_modes
|
||||
else [self.hvac_mode] # mode read-only -> fall back to only current mode
|
||||
)
|
||||
|
||||
@property
|
||||
def hvac_action(self) -> HVACAction | None:
|
||||
@@ -354,3 +359,13 @@ class KNXClimate(KnxYamlEntity, ClimateEntity):
|
||||
self._device.mode.unregister_device_updated_cb(self.after_update_callback)
|
||||
self._device.mode.xknx.devices.async_remove(self._device.mode)
|
||||
await super().async_will_remove_from_hass()
|
||||
|
||||
def after_update_callback(self, _device: XknxDevice) -> None:
|
||||
"""Call after device was updated."""
|
||||
if self._device.mode is not None and self._device.mode.supports_controller_mode:
|
||||
hvac_mode = CONTROLLER_MODES.get(
|
||||
self._device.mode.controller_mode, self.default_hvac_mode
|
||||
)
|
||||
if hvac_mode is not HVACMode.OFF:
|
||||
self._last_hvac_mode = hvac_mode
|
||||
super().after_update_callback(_device)
|
||||
|
||||
@@ -226,7 +226,7 @@ def _create_ui_light(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxLight
|
||||
group_address_color_temp_state = None
|
||||
color_temperature_type = ColorTemperatureType.UINT_2_BYTE
|
||||
if ga_color_temp := knx_config.get(CONF_GA_COLOR_TEMP):
|
||||
if ga_color_temp[CONF_DPT] == ColorTempModes.RELATIVE:
|
||||
if ga_color_temp[CONF_DPT] == ColorTempModes.RELATIVE.value:
|
||||
group_address_tunable_white = ga_color_temp[CONF_GA_WRITE]
|
||||
group_address_tunable_white_state = [
|
||||
ga_color_temp[CONF_GA_STATE],
|
||||
@@ -239,7 +239,7 @@ def _create_ui_light(xknx: XKNX, knx_config: ConfigType, name: str) -> XknxLight
|
||||
ga_color_temp[CONF_GA_STATE],
|
||||
*ga_color_temp[CONF_GA_PASSIVE],
|
||||
]
|
||||
if ga_color_temp[CONF_DPT] == ColorTempModes.ABSOLUTE_FLOAT:
|
||||
if ga_color_temp[CONF_DPT] == ColorTempModes.ABSOLUTE_FLOAT.value:
|
||||
color_temperature_type = ColorTemperatureType.FLOAT_2_BYTE
|
||||
|
||||
_color_dpt = get_dpt(CONF_GA_COLOR)
|
||||
|
||||
@@ -11,9 +11,9 @@
|
||||
"loggers": ["xknx", "xknxproject"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"xknx==3.0.0",
|
||||
"xknx==3.1.0",
|
||||
"xknxproject==3.7.1",
|
||||
"knx-frontend==2024.8.6.211307"
|
||||
"knx-frontend==2024.8.9.225351"
|
||||
],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/lacrosse_view",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["lacrosse_view"],
|
||||
"requirements": ["lacrosse-view==1.0.1"]
|
||||
"requirements": ["lacrosse-view==1.0.2"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/lcn",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pypck"],
|
||||
"requirements": ["pypck==0.7.17"]
|
||||
"requirements": ["pypck==0.7.20"]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
},
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pylutron_caseta"],
|
||||
"requirements": ["pylutron-caseta==0.20.0"],
|
||||
"requirements": ["pylutron-caseta==0.21.1"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_lutron._tcp.local.",
|
||||
|
||||
@@ -277,4 +277,15 @@ class MadvrSensor(MadVREntity, SensorEntity):
|
||||
@property
|
||||
def native_value(self) -> float | str | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator)
|
||||
val = self.entity_description.value_fn(self.coordinator)
|
||||
# check if sensor is enum
|
||||
if self.entity_description.device_class == SensorDeviceClass.ENUM:
|
||||
if (
|
||||
self.entity_description.options
|
||||
and val in self.entity_description.options
|
||||
):
|
||||
return val
|
||||
# return None for values that are not in the options
|
||||
return None
|
||||
|
||||
return val
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/mealie",
|
||||
"integration_type": "service",
|
||||
"iot_class": "local_polling",
|
||||
"requirements": ["aiomealie==0.8.0"]
|
||||
"requirements": ["aiomealie==0.8.1"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/monzo",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["monzopy==1.3.0"]
|
||||
"requirements": ["monzopy==1.3.2"]
|
||||
}
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["google_nest_sdm"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["google-nest-sdm==4.0.5"]
|
||||
"requirements": ["google-nest-sdm==4.0.6"]
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/nextbus",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["py_nextbus"],
|
||||
"requirements": ["py-nextbusnext==2.0.3"]
|
||||
"requirements": ["py-nextbusnext==2.0.4"]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from pyopenweathermap import OWMClient
|
||||
from pyopenweathermap import create_owm_client
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
@@ -33,6 +33,7 @@ class OpenweathermapData:
|
||||
"""Runtime data definition."""
|
||||
|
||||
name: str
|
||||
mode: str
|
||||
coordinator: WeatherUpdateCoordinator
|
||||
|
||||
|
||||
@@ -52,7 +53,7 @@ async def async_setup_entry(
|
||||
else:
|
||||
async_delete_issue(hass, entry.entry_id)
|
||||
|
||||
owm_client = OWMClient(api_key, mode, lang=language)
|
||||
owm_client = create_owm_client(api_key, mode, lang=language)
|
||||
weather_coordinator = WeatherUpdateCoordinator(
|
||||
owm_client, latitude, longitude, hass
|
||||
)
|
||||
@@ -61,7 +62,7 @@ async def async_setup_entry(
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(async_update_options))
|
||||
|
||||
entry.runtime_data = OpenweathermapData(name, weather_coordinator)
|
||||
entry.runtime_data = OpenweathermapData(name, mode, weather_coordinator)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
|
||||
@@ -58,10 +58,17 @@ FORECAST_MODE_DAILY = "daily"
|
||||
FORECAST_MODE_FREE_DAILY = "freedaily"
|
||||
FORECAST_MODE_ONECALL_HOURLY = "onecall_hourly"
|
||||
FORECAST_MODE_ONECALL_DAILY = "onecall_daily"
|
||||
OWM_MODE_V25 = "v2.5"
|
||||
OWM_MODE_FREE_CURRENT = "current"
|
||||
OWM_MODE_FREE_FORECAST = "forecast"
|
||||
OWM_MODE_V30 = "v3.0"
|
||||
OWM_MODES = [OWM_MODE_V30, OWM_MODE_V25]
|
||||
DEFAULT_OWM_MODE = OWM_MODE_V30
|
||||
OWM_MODE_V25 = "v2.5"
|
||||
OWM_MODES = [
|
||||
OWM_MODE_FREE_CURRENT,
|
||||
OWM_MODE_FREE_FORECAST,
|
||||
OWM_MODE_V30,
|
||||
OWM_MODE_V25,
|
||||
]
|
||||
DEFAULT_OWM_MODE = OWM_MODE_FREE_CURRENT
|
||||
|
||||
LANGUAGES = [
|
||||
"af",
|
||||
|
||||
@@ -86,8 +86,14 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
"""Format the weather response correctly."""
|
||||
_LOGGER.debug("OWM weather response: %s", weather_report)
|
||||
|
||||
current_weather = (
|
||||
self._get_current_weather_data(weather_report.current)
|
||||
if weather_report.current is not None
|
||||
else {}
|
||||
)
|
||||
|
||||
return {
|
||||
ATTR_API_CURRENT: self._get_current_weather_data(weather_report.current),
|
||||
ATTR_API_CURRENT: current_weather,
|
||||
ATTR_API_HOURLY_FORECAST: [
|
||||
self._get_hourly_forecast_weather_data(item)
|
||||
for item in weather_report.hourly_forecast
|
||||
@@ -122,6 +128,8 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
}
|
||||
|
||||
def _get_hourly_forecast_weather_data(self, forecast: HourlyWeatherForecast):
|
||||
uv_index = float(forecast.uv_index) if forecast.uv_index is not None else None
|
||||
|
||||
return Forecast(
|
||||
datetime=forecast.date_time.isoformat(),
|
||||
condition=self._get_condition(forecast.condition.id),
|
||||
@@ -134,12 +142,14 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
wind_speed=forecast.wind_speed,
|
||||
native_wind_gust_speed=forecast.wind_gust,
|
||||
wind_bearing=forecast.wind_bearing,
|
||||
uv_index=float(forecast.uv_index),
|
||||
uv_index=uv_index,
|
||||
precipitation_probability=round(forecast.precipitation_probability * 100),
|
||||
precipitation=self._calc_precipitation(forecast.rain, forecast.snow),
|
||||
)
|
||||
|
||||
def _get_daily_forecast_weather_data(self, forecast: DailyWeatherForecast):
|
||||
uv_index = float(forecast.uv_index) if forecast.uv_index is not None else None
|
||||
|
||||
return Forecast(
|
||||
datetime=forecast.date_time.isoformat(),
|
||||
condition=self._get_condition(forecast.condition.id),
|
||||
@@ -153,7 +163,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator):
|
||||
wind_speed=forecast.wind_speed,
|
||||
native_wind_gust_speed=forecast.wind_gust,
|
||||
wind_bearing=forecast.wind_bearing,
|
||||
uv_index=float(forecast.uv_index),
|
||||
uv_index=uv_index,
|
||||
precipitation_probability=round(forecast.precipitation_probability * 100),
|
||||
precipitation=round(forecast.rain + forecast.snow, 2),
|
||||
)
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/openweathermap",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyopenweathermap"],
|
||||
"requirements": ["pyopenweathermap==0.0.9"]
|
||||
"requirements": ["pyopenweathermap==0.1.1"]
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ from homeassistant.const import (
|
||||
UnitOfVolumetricFlux,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
@@ -47,6 +48,7 @@ from .const import (
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
MANUFACTURER,
|
||||
OWM_MODE_FREE_FORECAST,
|
||||
)
|
||||
from .coordinator import WeatherUpdateCoordinator
|
||||
|
||||
@@ -161,16 +163,23 @@ async def async_setup_entry(
|
||||
name = domain_data.name
|
||||
weather_coordinator = domain_data.coordinator
|
||||
|
||||
entities: list[AbstractOpenWeatherMapSensor] = [
|
||||
OpenWeatherMapSensor(
|
||||
name,
|
||||
f"{config_entry.unique_id}-{description.key}",
|
||||
description,
|
||||
weather_coordinator,
|
||||
if domain_data.mode == OWM_MODE_FREE_FORECAST:
|
||||
entity_registry = er.async_get(hass)
|
||||
entries = er.async_entries_for_config_entry(
|
||||
entity_registry, config_entry.entry_id
|
||||
)
|
||||
for entry in entries:
|
||||
entity_registry.async_remove(entry.entity_id)
|
||||
else:
|
||||
async_add_entities(
|
||||
OpenWeatherMapSensor(
|
||||
name,
|
||||
f"{config_entry.unique_id}-{description.key}",
|
||||
description,
|
||||
weather_coordinator,
|
||||
)
|
||||
for description in WEATHER_SENSOR_TYPES
|
||||
)
|
||||
for description in WEATHER_SENSOR_TYPES
|
||||
]
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class AbstractOpenWeatherMapSensor(SensorEntity):
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pyopenweathermap import OWMClient, RequestError
|
||||
from pyopenweathermap import RequestError, create_owm_client
|
||||
|
||||
from homeassistant.const import CONF_LANGUAGE, CONF_MODE
|
||||
|
||||
@@ -16,7 +16,7 @@ async def validate_api_key(api_key, mode):
|
||||
api_key_valid = None
|
||||
errors, description_placeholders = {}, {}
|
||||
try:
|
||||
owm_client = OWMClient(api_key, mode)
|
||||
owm_client = create_owm_client(api_key, mode)
|
||||
api_key_valid = await owm_client.validate_key()
|
||||
except RequestError as error:
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
@@ -8,6 +8,7 @@ from homeassistant.components.weather import (
|
||||
WeatherEntityFeature,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
UnitOfLength,
|
||||
UnitOfPrecipitationDepth,
|
||||
UnitOfPressure,
|
||||
UnitOfSpeed,
|
||||
@@ -29,6 +30,7 @@ from .const import (
|
||||
ATTR_API_HUMIDITY,
|
||||
ATTR_API_PRESSURE,
|
||||
ATTR_API_TEMPERATURE,
|
||||
ATTR_API_VISIBILITY_DISTANCE,
|
||||
ATTR_API_WIND_BEARING,
|
||||
ATTR_API_WIND_GUST,
|
||||
ATTR_API_WIND_SPEED,
|
||||
@@ -36,6 +38,9 @@ from .const import (
|
||||
DEFAULT_NAME,
|
||||
DOMAIN,
|
||||
MANUFACTURER,
|
||||
OWM_MODE_FREE_FORECAST,
|
||||
OWM_MODE_V25,
|
||||
OWM_MODE_V30,
|
||||
)
|
||||
from .coordinator import WeatherUpdateCoordinator
|
||||
|
||||
@@ -48,10 +53,11 @@ async def async_setup_entry(
|
||||
"""Set up OpenWeatherMap weather entity based on a config entry."""
|
||||
domain_data = config_entry.runtime_data
|
||||
name = domain_data.name
|
||||
mode = domain_data.mode
|
||||
weather_coordinator = domain_data.coordinator
|
||||
|
||||
unique_id = f"{config_entry.unique_id}"
|
||||
owm_weather = OpenWeatherMapWeather(name, unique_id, weather_coordinator)
|
||||
owm_weather = OpenWeatherMapWeather(name, unique_id, mode, weather_coordinator)
|
||||
|
||||
async_add_entities([owm_weather], False)
|
||||
|
||||
@@ -66,11 +72,13 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordina
|
||||
_attr_native_pressure_unit = UnitOfPressure.HPA
|
||||
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
_attr_native_wind_speed_unit = UnitOfSpeed.METERS_PER_SECOND
|
||||
_attr_native_visibility_unit = UnitOfLength.METERS
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
unique_id: str,
|
||||
mode: str,
|
||||
weather_coordinator: WeatherUpdateCoordinator,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
@@ -83,59 +91,71 @@ class OpenWeatherMapWeather(SingleCoordinatorWeatherEntity[WeatherUpdateCoordina
|
||||
manufacturer=MANUFACTURER,
|
||||
name=DEFAULT_NAME,
|
||||
)
|
||||
self._attr_supported_features = (
|
||||
WeatherEntityFeature.FORECAST_DAILY | WeatherEntityFeature.FORECAST_HOURLY
|
||||
)
|
||||
|
||||
if mode in (OWM_MODE_V30, OWM_MODE_V25):
|
||||
self._attr_supported_features = (
|
||||
WeatherEntityFeature.FORECAST_DAILY
|
||||
| WeatherEntityFeature.FORECAST_HOURLY
|
||||
)
|
||||
elif mode == OWM_MODE_FREE_FORECAST:
|
||||
self._attr_supported_features = WeatherEntityFeature.FORECAST_HOURLY
|
||||
|
||||
@property
|
||||
def condition(self) -> str | None:
|
||||
"""Return the current condition."""
|
||||
return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_CONDITION]
|
||||
return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_CONDITION)
|
||||
|
||||
@property
|
||||
def cloud_coverage(self) -> float | None:
|
||||
"""Return the Cloud coverage in %."""
|
||||
return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_CLOUDS]
|
||||
return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_CLOUDS)
|
||||
|
||||
@property
|
||||
def native_apparent_temperature(self) -> float | None:
|
||||
"""Return the apparent temperature."""
|
||||
return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_FEELS_LIKE_TEMPERATURE]
|
||||
return self.coordinator.data[ATTR_API_CURRENT].get(
|
||||
ATTR_API_FEELS_LIKE_TEMPERATURE
|
||||
)
|
||||
|
||||
@property
|
||||
def native_temperature(self) -> float | None:
|
||||
"""Return the temperature."""
|
||||
return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_TEMPERATURE]
|
||||
return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_TEMPERATURE)
|
||||
|
||||
@property
|
||||
def native_pressure(self) -> float | None:
|
||||
"""Return the pressure."""
|
||||
return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_PRESSURE]
|
||||
return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_PRESSURE)
|
||||
|
||||
@property
|
||||
def humidity(self) -> float | None:
|
||||
"""Return the humidity."""
|
||||
return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_HUMIDITY]
|
||||
return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_HUMIDITY)
|
||||
|
||||
@property
|
||||
def native_dew_point(self) -> float | None:
|
||||
"""Return the dew point."""
|
||||
return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_DEW_POINT]
|
||||
return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_DEW_POINT)
|
||||
|
||||
@property
|
||||
def native_wind_gust_speed(self) -> float | None:
|
||||
"""Return the wind gust speed."""
|
||||
return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_WIND_GUST]
|
||||
return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_WIND_GUST)
|
||||
|
||||
@property
|
||||
def native_wind_speed(self) -> float | None:
|
||||
"""Return the wind speed."""
|
||||
return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_WIND_SPEED]
|
||||
return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_WIND_SPEED)
|
||||
|
||||
@property
|
||||
def wind_bearing(self) -> float | str | None:
|
||||
"""Return the wind bearing."""
|
||||
return self.coordinator.data[ATTR_API_CURRENT][ATTR_API_WIND_BEARING]
|
||||
return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_WIND_BEARING)
|
||||
|
||||
@property
|
||||
def visibility(self) -> float | str | None:
|
||||
"""Return visibility."""
|
||||
return self.coordinator.data[ATTR_API_CURRENT].get(ATTR_API_VISIBILITY_DISTANCE)
|
||||
|
||||
@callback
|
||||
def _async_forecast_daily(self) -> list[Forecast] | None:
|
||||
|
||||
@@ -22,6 +22,7 @@ class PiHoleUpdateEntityDescription(UpdateEntityDescription):
|
||||
|
||||
installed_version: Callable[[dict], str | None] = lambda api: None
|
||||
latest_version: Callable[[dict], str | None] = lambda api: None
|
||||
has_update: Callable[[dict], bool | None] = lambda api: None
|
||||
release_base_url: str | None = None
|
||||
title: str | None = None
|
||||
|
||||
@@ -34,6 +35,7 @@ UPDATE_ENTITY_TYPES: tuple[PiHoleUpdateEntityDescription, ...] = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
installed_version=lambda versions: versions.get("core_current"),
|
||||
latest_version=lambda versions: versions.get("core_latest"),
|
||||
has_update=lambda versions: versions.get("core_update"),
|
||||
release_base_url="https://github.com/pi-hole/pi-hole/releases/tag",
|
||||
),
|
||||
PiHoleUpdateEntityDescription(
|
||||
@@ -43,6 +45,7 @@ UPDATE_ENTITY_TYPES: tuple[PiHoleUpdateEntityDescription, ...] = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
installed_version=lambda versions: versions.get("web_current"),
|
||||
latest_version=lambda versions: versions.get("web_latest"),
|
||||
has_update=lambda versions: versions.get("web_update"),
|
||||
release_base_url="https://github.com/pi-hole/AdminLTE/releases/tag",
|
||||
),
|
||||
PiHoleUpdateEntityDescription(
|
||||
@@ -52,6 +55,7 @@ UPDATE_ENTITY_TYPES: tuple[PiHoleUpdateEntityDescription, ...] = (
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
installed_version=lambda versions: versions.get("FTL_current"),
|
||||
latest_version=lambda versions: versions.get("FTL_latest"),
|
||||
has_update=lambda versions: versions.get("FTL_update"),
|
||||
release_base_url="https://github.com/pi-hole/FTL/releases/tag",
|
||||
),
|
||||
)
|
||||
@@ -110,7 +114,9 @@ class PiHoleUpdateEntity(PiHoleEntity, UpdateEntity):
|
||||
def latest_version(self) -> str | None:
|
||||
"""Latest version available for install."""
|
||||
if isinstance(self.api.versions, dict):
|
||||
return self.entity_description.latest_version(self.api.versions)
|
||||
if self.entity_description.has_update(self.api.versions):
|
||||
return self.entity_description.latest_version(self.api.versions)
|
||||
return self.installed_version
|
||||
return None
|
||||
|
||||
@property
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/qnap_qsw",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aioqsw"],
|
||||
"requirements": ["aioqsw==0.4.0"]
|
||||
"requirements": ["aioqsw==0.4.1"]
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ class LegacyBase(DeclarativeBase):
|
||||
"""Base class for tables, used for schema migration."""
|
||||
|
||||
|
||||
SCHEMA_VERSION = 44
|
||||
SCHEMA_VERSION = 45
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -632,7 +632,7 @@ def _update_states_table_with_foreign_key_options(
|
||||
|
||||
def _drop_foreign_key_constraints(
|
||||
session_maker: Callable[[], Session], engine: Engine, table: str, column: str
|
||||
) -> list[tuple[str, str, ReflectedForeignKeyConstraint]]:
|
||||
) -> tuple[bool, list[tuple[str, str, ReflectedForeignKeyConstraint]]]:
|
||||
"""Drop foreign key constraints for a table on specific columns."""
|
||||
inspector = sqlalchemy.inspect(engine)
|
||||
dropped_constraints = [
|
||||
@@ -649,6 +649,7 @@ def _drop_foreign_key_constraints(
|
||||
if foreign_key["name"] and foreign_key["constrained_columns"] == [column]
|
||||
]
|
||||
|
||||
fk_remove_ok = True
|
||||
for drop in drops:
|
||||
with session_scope(session=session_maker()) as session:
|
||||
try:
|
||||
@@ -660,40 +661,185 @@ def _drop_foreign_key_constraints(
|
||||
TABLE_STATES,
|
||||
column,
|
||||
)
|
||||
fk_remove_ok = False
|
||||
|
||||
return dropped_constraints
|
||||
return fk_remove_ok, dropped_constraints
|
||||
|
||||
|
||||
def _restore_foreign_key_constraints(
|
||||
session_maker: Callable[[], Session],
|
||||
engine: Engine,
|
||||
dropped_constraints: list[tuple[str, str, ReflectedForeignKeyConstraint]],
|
||||
foreign_columns: list[tuple[str, str, str | None, str | None]],
|
||||
) -> None:
|
||||
"""Restore foreign key constraints."""
|
||||
for table, column, dropped_constraint in dropped_constraints:
|
||||
for table, column, foreign_table, foreign_column in foreign_columns:
|
||||
constraints = Base.metadata.tables[table].foreign_key_constraints
|
||||
for constraint in constraints:
|
||||
if constraint.column_keys == [column]:
|
||||
break
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Did not find a matching constraint for %s", dropped_constraint
|
||||
)
|
||||
_LOGGER.info("Did not find a matching constraint for %s.%s", table, column)
|
||||
continue
|
||||
|
||||
if TYPE_CHECKING:
|
||||
assert foreign_table is not None
|
||||
assert foreign_column is not None
|
||||
|
||||
# AddConstraint mutates the constraint passed to it, we need to
|
||||
# undo that to avoid changing the behavior of the table schema.
|
||||
# https://github.com/sqlalchemy/sqlalchemy/blob/96f1172812f858fead45cdc7874abac76f45b339/lib/sqlalchemy/sql/ddl.py#L746-L748
|
||||
create_rule = constraint._create_rule # noqa: SLF001
|
||||
add_constraint = AddConstraint(constraint) # type: ignore[no-untyped-call]
|
||||
constraint._create_rule = create_rule # noqa: SLF001
|
||||
try:
|
||||
_add_constraint(session_maker, add_constraint, table, column)
|
||||
except IntegrityError:
|
||||
_LOGGER.exception(
|
||||
(
|
||||
"Could not update foreign options in %s table, will delete "
|
||||
"violations and try again"
|
||||
),
|
||||
table,
|
||||
)
|
||||
_delete_foreign_key_violations(
|
||||
session_maker, engine, table, column, foreign_table, foreign_column
|
||||
)
|
||||
_add_constraint(session_maker, add_constraint, table, column)
|
||||
|
||||
with session_scope(session=session_maker()) as session:
|
||||
try:
|
||||
connection = session.connection()
|
||||
connection.execute(add_constraint)
|
||||
except (InternalError, OperationalError):
|
||||
_LOGGER.exception("Could not update foreign options in %s table", table)
|
||||
|
||||
def _add_constraint(
|
||||
session_maker: Callable[[], Session],
|
||||
add_constraint: AddConstraint,
|
||||
table: str,
|
||||
column: str,
|
||||
) -> None:
|
||||
"""Add a foreign key constraint."""
|
||||
_LOGGER.warning(
|
||||
"Adding foreign key constraint to %s.%s. "
|
||||
"Note: this can take several minutes on large databases and slow "
|
||||
"machines. Please be patient!",
|
||||
table,
|
||||
column,
|
||||
)
|
||||
with session_scope(session=session_maker()) as session:
|
||||
try:
|
||||
connection = session.connection()
|
||||
connection.execute(add_constraint)
|
||||
except (InternalError, OperationalError):
|
||||
_LOGGER.exception("Could not update foreign options in %s table", table)
|
||||
|
||||
|
||||
def _delete_foreign_key_violations(
|
||||
session_maker: Callable[[], Session],
|
||||
engine: Engine,
|
||||
table: str,
|
||||
column: str,
|
||||
foreign_table: str,
|
||||
foreign_column: str,
|
||||
) -> None:
|
||||
"""Remove rows which violate the constraints."""
|
||||
if engine.dialect.name not in (SupportedDialect.MYSQL, SupportedDialect.POSTGRESQL):
|
||||
raise RuntimeError(
|
||||
f"_delete_foreign_key_violations not supported for {engine.dialect.name}"
|
||||
)
|
||||
|
||||
_LOGGER.warning(
|
||||
"Rows in table %s where %s references non existing %s.%s will be %s. "
|
||||
"Note: this can take several minutes on large databases and slow "
|
||||
"machines. Please be patient!",
|
||||
table,
|
||||
column,
|
||||
foreign_table,
|
||||
foreign_column,
|
||||
"set to NULL" if table == foreign_table else "deleted",
|
||||
)
|
||||
|
||||
result: CursorResult | None = None
|
||||
if table == foreign_table:
|
||||
# In case of a foreign reference to the same table, we set invalid
|
||||
# references to NULL instead of deleting as deleting rows may
|
||||
# cause additional invalid references to be created. This is to handle
|
||||
# old_state_id referencing a missing state.
|
||||
if engine.dialect.name == SupportedDialect.MYSQL:
|
||||
while result is None or result.rowcount > 0:
|
||||
with session_scope(session=session_maker()) as session:
|
||||
# The subquery (SELECT {foreign_column} from {foreign_table}) is
|
||||
# to be compatible with old MySQL versions which do not allow
|
||||
# referencing the table being updated in the WHERE clause.
|
||||
result = session.connection().execute(
|
||||
text(
|
||||
f"UPDATE {table} as t1 " # noqa: S608
|
||||
f"SET {column} = NULL "
|
||||
"WHERE ("
|
||||
f"t1.{column} IS NOT NULL AND "
|
||||
"NOT EXISTS "
|
||||
"(SELECT 1 "
|
||||
f"FROM (SELECT {foreign_column} from {foreign_table}) AS t2 "
|
||||
f"WHERE t2.{foreign_column} = t1.{column})) "
|
||||
"LIMIT 100000;"
|
||||
)
|
||||
)
|
||||
elif engine.dialect.name == SupportedDialect.POSTGRESQL:
|
||||
while result is None or result.rowcount > 0:
|
||||
with session_scope(session=session_maker()) as session:
|
||||
# PostgreSQL does not support LIMIT in UPDATE clauses, so we
|
||||
# update matches from a limited subquery instead.
|
||||
result = session.connection().execute(
|
||||
text(
|
||||
f"UPDATE {table} " # noqa: S608
|
||||
f"SET {column} = NULL "
|
||||
f"WHERE {column} in "
|
||||
f"(SELECT {column} from {table} as t1 "
|
||||
"WHERE ("
|
||||
f"t1.{column} IS NOT NULL AND "
|
||||
"NOT EXISTS "
|
||||
"(SELECT 1 "
|
||||
f"FROM {foreign_table} AS t2 "
|
||||
f"WHERE t2.{foreign_column} = t1.{column})) "
|
||||
"LIMIT 100000);"
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if engine.dialect.name == SupportedDialect.MYSQL:
|
||||
while result is None or result.rowcount > 0:
|
||||
with session_scope(session=session_maker()) as session:
|
||||
result = session.connection().execute(
|
||||
# We don't use an alias for the table we're deleting from,
|
||||
# support of the form `DELETE FROM table AS t1` was added in
|
||||
# MariaDB 11.6 and is not supported by MySQL. Those engines
|
||||
# instead support the from `DELETE t1 from table AS t1` which
|
||||
# is not supported by PostgreSQL and undocumented for MariaDB.
|
||||
text(
|
||||
f"DELETE FROM {table} " # noqa: S608
|
||||
"WHERE ("
|
||||
f"{table}.{column} IS NOT NULL AND "
|
||||
"NOT EXISTS "
|
||||
"(SELECT 1 "
|
||||
f"FROM {foreign_table} AS t2 "
|
||||
f"WHERE t2.{foreign_column} = {table}.{column})) "
|
||||
"LIMIT 100000;"
|
||||
)
|
||||
)
|
||||
elif engine.dialect.name == SupportedDialect.POSTGRESQL:
|
||||
while result is None or result.rowcount > 0:
|
||||
with session_scope(session=session_maker()) as session:
|
||||
# PostgreSQL does not support LIMIT in DELETE clauses, so we
|
||||
# delete matches from a limited subquery instead.
|
||||
result = session.connection().execute(
|
||||
text(
|
||||
f"DELETE FROM {table} " # noqa: S608
|
||||
f"WHERE {column} in "
|
||||
f"(SELECT {column} from {table} as t1 "
|
||||
"WHERE ("
|
||||
f"t1.{column} IS NOT NULL AND "
|
||||
"NOT EXISTS "
|
||||
"(SELECT 1 "
|
||||
f"FROM {foreign_table} AS t2 "
|
||||
f"WHERE t2.{foreign_column} = t1.{column})) "
|
||||
"LIMIT 100000);"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@database_job_retry_wrapper("Apply migration update", 10)
|
||||
@@ -1457,6 +1603,38 @@ class _SchemaVersion43Migrator(_SchemaVersionMigrator, target_version=43):
|
||||
)
|
||||
|
||||
|
||||
FOREIGN_COLUMNS = (
|
||||
(
|
||||
"events",
|
||||
("data_id", "event_type_id"),
|
||||
(
|
||||
("data_id", "event_data", "data_id"),
|
||||
("event_type_id", "event_types", "event_type_id"),
|
||||
),
|
||||
),
|
||||
(
|
||||
"states",
|
||||
("event_id", "old_state_id", "attributes_id", "metadata_id"),
|
||||
(
|
||||
("event_id", None, None),
|
||||
("old_state_id", "states", "state_id"),
|
||||
("attributes_id", "state_attributes", "attributes_id"),
|
||||
("metadata_id", "states_meta", "metadata_id"),
|
||||
),
|
||||
),
|
||||
(
|
||||
"statistics",
|
||||
("metadata_id",),
|
||||
(("metadata_id", "statistics_meta", "id"),),
|
||||
),
|
||||
(
|
||||
"statistics_short_term",
|
||||
("metadata_id",),
|
||||
(("metadata_id", "statistics_meta", "id"),),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class _SchemaVersion44Migrator(_SchemaVersionMigrator, target_version=44):
|
||||
def _apply_update(self) -> None:
|
||||
"""Version specific update method."""
|
||||
@@ -1469,24 +1647,14 @@ class _SchemaVersion44Migrator(_SchemaVersionMigrator, target_version=44):
|
||||
else ""
|
||||
)
|
||||
# First drop foreign key constraints
|
||||
foreign_columns = (
|
||||
("events", ("data_id", "event_type_id")),
|
||||
("states", ("event_id", "old_state_id", "attributes_id", "metadata_id")),
|
||||
("statistics", ("metadata_id",)),
|
||||
("statistics_short_term", ("metadata_id",)),
|
||||
)
|
||||
dropped_constraints = [
|
||||
dropped_constraint
|
||||
for table, columns in foreign_columns
|
||||
for column in columns
|
||||
for dropped_constraint in _drop_foreign_key_constraints(
|
||||
self.session_maker, self.engine, table, column
|
||||
)
|
||||
]
|
||||
_LOGGER.debug("Dropped foreign key constraints: %s", dropped_constraints)
|
||||
for table, columns, _ in FOREIGN_COLUMNS:
|
||||
for column in columns:
|
||||
_drop_foreign_key_constraints(
|
||||
self.session_maker, self.engine, table, column
|
||||
)
|
||||
|
||||
# Then modify the constrained columns
|
||||
for table, columns in foreign_columns:
|
||||
for table, columns, _ in FOREIGN_COLUMNS:
|
||||
_modify_columns(
|
||||
self.session_maker,
|
||||
self.engine,
|
||||
@@ -1516,9 +1684,24 @@ class _SchemaVersion44Migrator(_SchemaVersionMigrator, target_version=44):
|
||||
table,
|
||||
[f"{column} {BIG_INTEGER_SQL} {identity_sql}"],
|
||||
)
|
||||
# Finally restore dropped constraints
|
||||
|
||||
|
||||
class _SchemaVersion45Migrator(_SchemaVersionMigrator, target_version=45):
|
||||
def _apply_update(self) -> None:
|
||||
"""Version specific update method."""
|
||||
# We skip this step for SQLITE, it doesn't have differently sized integers
|
||||
if self.engine.dialect.name == SupportedDialect.SQLITE:
|
||||
return
|
||||
|
||||
# Restore constraints dropped in migration to schema version 44
|
||||
_restore_foreign_key_constraints(
|
||||
self.session_maker, self.engine, dropped_constraints
|
||||
self.session_maker,
|
||||
self.engine,
|
||||
[
|
||||
(table, column, foreign_table, foreign_column)
|
||||
for table, _, foreign_mappings in FOREIGN_COLUMNS
|
||||
for column, foreign_table, foreign_column in foreign_mappings
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@@ -1956,14 +2139,15 @@ def cleanup_legacy_states_event_ids(instance: Recorder) -> bool:
|
||||
if instance.dialect_name == SupportedDialect.SQLITE:
|
||||
# SQLite does not support dropping foreign key constraints
|
||||
# so we have to rebuild the table
|
||||
rebuild_sqlite_table(session_maker, instance.engine, States)
|
||||
fk_remove_ok = rebuild_sqlite_table(session_maker, instance.engine, States)
|
||||
else:
|
||||
_drop_foreign_key_constraints(
|
||||
fk_remove_ok, _ = _drop_foreign_key_constraints(
|
||||
session_maker, instance.engine, TABLE_STATES, "event_id"
|
||||
)
|
||||
_drop_index(session_maker, "states", LEGACY_STATES_EVENT_ID_INDEX)
|
||||
instance.use_legacy_events_index = False
|
||||
_mark_migration_done(session, EventIDPostMigration)
|
||||
if fk_remove_ok:
|
||||
_drop_index(session_maker, "states", LEGACY_STATES_EVENT_ID_INDEX)
|
||||
instance.use_legacy_events_index = False
|
||||
_mark_migration_done(session, EventIDPostMigration)
|
||||
|
||||
return True
|
||||
|
||||
@@ -2419,6 +2603,7 @@ class EventIDPostMigration(BaseRunTimeMigration):
|
||||
|
||||
migration_id = "event_id_post_migration"
|
||||
task = MigrationTask
|
||||
migration_version = 2
|
||||
|
||||
@staticmethod
|
||||
def migrate_data(instance: Recorder) -> bool:
|
||||
@@ -2469,7 +2654,7 @@ def _mark_migration_done(
|
||||
|
||||
def rebuild_sqlite_table(
|
||||
session_maker: Callable[[], Session], engine: Engine, table: type[Base]
|
||||
) -> None:
|
||||
) -> bool:
|
||||
"""Rebuild an SQLite table.
|
||||
|
||||
This must only be called after all migrations are complete
|
||||
@@ -2524,8 +2709,10 @@ def rebuild_sqlite_table(
|
||||
# Swallow the exception since we do not want to ever raise
|
||||
# an integrity error as it would cause the database
|
||||
# to be discarded and recreated from scratch
|
||||
return False
|
||||
else:
|
||||
_LOGGER.warning("Rebuilding SQLite table %s finished", orig_name)
|
||||
return True
|
||||
finally:
|
||||
with session_scope(session=session_maker()) as session:
|
||||
# Step 12 - Re-enable foreign keys
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/russound_rio",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiorussound"],
|
||||
"requirements": ["aiorussound==2.2.0"]
|
||||
"requirements": ["aiorussound==2.2.3"]
|
||||
}
|
||||
|
||||
@@ -128,11 +128,18 @@ class RussoundZoneDevice(MediaPlayerEntity):
|
||||
self._zone = zone
|
||||
self._sources = sources
|
||||
self._attr_name = zone.name
|
||||
self._attr_unique_id = f"{self._controller.mac_address}-{zone.device_str()}"
|
||||
primary_mac_address = (
|
||||
self._controller.mac_address
|
||||
or self._controller.parent_controller.mac_address
|
||||
)
|
||||
self._attr_unique_id = f"{primary_mac_address}-{zone.device_str()}"
|
||||
device_identifier = (
|
||||
self._controller.mac_address
|
||||
or f"{primary_mac_address}-{self._controller.controller_id}"
|
||||
)
|
||||
self._attr_device_info = DeviceInfo(
|
||||
# Use MAC address of Russound device as identifier
|
||||
identifiers={(DOMAIN, self._controller.mac_address)},
|
||||
connections={(CONNECTION_NETWORK_MAC, self._controller.mac_address)},
|
||||
identifiers={(DOMAIN, device_identifier)},
|
||||
manufacturer="Russound",
|
||||
name=self._controller.controller_type,
|
||||
model=self._controller.controller_type,
|
||||
@@ -143,6 +150,10 @@ class RussoundZoneDevice(MediaPlayerEntity):
|
||||
DOMAIN,
|
||||
self._controller.parent_controller.mac_address,
|
||||
)
|
||||
else:
|
||||
self._attr_device_info["connections"] = {
|
||||
(CONNECTION_NETWORK_MAC, self._controller.mac_address)
|
||||
}
|
||||
for flag, feature in MP_FEATURES_BY_FLAG.items():
|
||||
if flag in zone.instance.supported_features:
|
||||
self._attr_supported_features |= feature
|
||||
|
||||
@@ -5,5 +5,5 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/schlage",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["pyschlage==2024.6.0"]
|
||||
"requirements": ["pyschlage==2024.8.0"]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioshelly"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aioshelly==11.1.0"],
|
||||
"requirements": ["aioshelly==11.2.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"type": "_http._tcp.local.",
|
||||
|
||||
@@ -44,7 +44,7 @@ async def async_setup_entry(
|
||||
translation_key="request_timeout",
|
||||
translation_placeholders={
|
||||
"config_title": entry.title,
|
||||
"error": e,
|
||||
"error": str(e),
|
||||
},
|
||||
) from e
|
||||
except OpendataTransportError as e:
|
||||
@@ -54,7 +54,7 @@ async def async_setup_entry(
|
||||
translation_placeholders={
|
||||
**PLACEHOLDERS,
|
||||
"config_title": entry.title,
|
||||
"error": e,
|
||||
"error": str(e),
|
||||
},
|
||||
) from e
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["synology_dsm"],
|
||||
"requirements": ["py-synologydsm-api==2.4.4"],
|
||||
"requirements": ["py-synologydsm-api==2.4.5"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Synology",
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["systembridgeconnector"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["systembridgeconnector==4.1.0", "systembridgemodels==4.1.0"],
|
||||
"requirements": ["systembridgeconnector==4.1.5", "systembridgemodels==4.2.4"],
|
||||
"zeroconf": ["_system-bridge._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -168,13 +168,13 @@ class TessieRearTrunkEntity(TessieEntity, CoverEntity):
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open rear trunk."""
|
||||
if self._value == TessieCoverStates.CLOSED:
|
||||
if self.is_closed:
|
||||
await self.run(open_close_rear_trunk)
|
||||
self.set((self.key, TessieCoverStates.OPEN))
|
||||
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close rear trunk."""
|
||||
if self._value == TessieCoverStates.OPEN:
|
||||
if not self.is_closed:
|
||||
await self.run(open_close_rear_trunk)
|
||||
self.set((self.key, TessieCoverStates.CLOSED))
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/tessie",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tessie"],
|
||||
"loggers": ["tessie", "tesla-fleet-api"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==0.7.3"]
|
||||
}
|
||||
|
||||
@@ -55,12 +55,12 @@ class TransmissionDataUpdateCoordinator(DataUpdateCoordinator[SessionStats]):
|
||||
@property
|
||||
def limit(self) -> int:
|
||||
"""Return limit."""
|
||||
return self.config_entry.data.get(CONF_LIMIT, DEFAULT_LIMIT)
|
||||
return self.config_entry.options.get(CONF_LIMIT, DEFAULT_LIMIT)
|
||||
|
||||
@property
|
||||
def order(self) -> str:
|
||||
"""Return order."""
|
||||
return self.config_entry.data.get(CONF_ORDER, DEFAULT_ORDER)
|
||||
return self.config_entry.options.get(CONF_ORDER, DEFAULT_ORDER)
|
||||
|
||||
async def _async_update_data(self) -> SessionStats:
|
||||
"""Update transmission data."""
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aiounifi"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["aiounifi==79"],
|
||||
"requirements": ["aiounifi==80"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Ubiquiti Networks",
|
||||
|
||||
@@ -15,8 +15,6 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -62,9 +60,8 @@ class WolButton(ButtonEntity):
|
||||
self._attr_unique_id = dr.format_mac(mac_address)
|
||||
self._attr_device_info = dr.DeviceInfo(
|
||||
connections={(dr.CONNECTION_NETWORK_MAC, self._attr_unique_id)},
|
||||
identifiers={(DOMAIN, self._attr_unique_id)},
|
||||
manufacturer="Wake on LAN",
|
||||
name=name,
|
||||
default_manufacturer="Wake on LAN",
|
||||
default_name=name,
|
||||
)
|
||||
|
||||
async def async_press(self) -> None:
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["wled==0.20.1"],
|
||||
"requirements": ["wled==0.20.2"],
|
||||
"zeroconf": ["_wled._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Constants for the Yamaha component."""
|
||||
|
||||
DOMAIN = "yamaha"
|
||||
DISCOVER_TIMEOUT = 3
|
||||
KNOWN_ZONES = "known_zones"
|
||||
CURSOR_TYPE_DOWN = "down"
|
||||
CURSOR_TYPE_LEFT = "left"
|
||||
|
||||
@@ -31,6 +31,7 @@ from .const import (
|
||||
CURSOR_TYPE_RIGHT,
|
||||
CURSOR_TYPE_SELECT,
|
||||
CURSOR_TYPE_UP,
|
||||
DISCOVER_TIMEOUT,
|
||||
DOMAIN,
|
||||
KNOWN_ZONES,
|
||||
SERVICE_ENABLE_OUTPUT,
|
||||
@@ -125,18 +126,33 @@ def _discovery(config_info):
|
||||
elif config_info.host is None:
|
||||
_LOGGER.debug("Config No Host Supplied Zones")
|
||||
zones = []
|
||||
for recv in rxv.find():
|
||||
for recv in rxv.find(DISCOVER_TIMEOUT):
|
||||
zones.extend(recv.zone_controllers())
|
||||
else:
|
||||
_LOGGER.debug("Config Zones")
|
||||
zones = None
|
||||
|
||||
# Fix for upstream issues in rxv.find() with some hardware.
|
||||
with contextlib.suppress(AttributeError):
|
||||
for recv in rxv.find():
|
||||
with contextlib.suppress(AttributeError, ValueError):
|
||||
for recv in rxv.find(DISCOVER_TIMEOUT):
|
||||
_LOGGER.debug(
|
||||
"Found Serial %s %s %s",
|
||||
recv.serial_number,
|
||||
recv.ctrl_url,
|
||||
recv.zone,
|
||||
)
|
||||
if recv.ctrl_url == config_info.ctrl_url:
|
||||
_LOGGER.debug("Config Zones Matched %s", config_info.ctrl_url)
|
||||
zones = recv.zone_controllers()
|
||||
_LOGGER.debug(
|
||||
"Config Zones Matched Serial %s: %s",
|
||||
recv.ctrl_url,
|
||||
recv.serial_number,
|
||||
)
|
||||
zones = rxv.RXV(
|
||||
config_info.ctrl_url,
|
||||
friendly_name=config_info.name,
|
||||
serial_number=recv.serial_number,
|
||||
model_name=recv.model_name,
|
||||
).zone_controllers()
|
||||
break
|
||||
|
||||
if not zones:
|
||||
@@ -170,7 +186,7 @@ async def async_setup_platform(
|
||||
|
||||
entities = []
|
||||
for zctrl in zone_ctrls:
|
||||
_LOGGER.debug("Receiver zone: %s", zctrl.zone)
|
||||
_LOGGER.debug("Receiver zone: %s serial %s", zctrl.zone, zctrl.serial_number)
|
||||
if config_info.zone_ignore and zctrl.zone in config_info.zone_ignore:
|
||||
_LOGGER.debug("Ignore receiver zone: %s %s", config_info.name, zctrl.zone)
|
||||
continue
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"dependencies": ["auth", "application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/yolink",
|
||||
"iot_class": "cloud_push",
|
||||
"requirements": ["yolink-api==0.4.6"]
|
||||
"requirements": ["yolink-api==0.4.7"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import voluptuous as vol
|
||||
from zha.application.const import BAUD_RATES, RadioType
|
||||
@@ -12,8 +13,13 @@ from zigpy.config import CONF_DATABASE, CONF_DEVICE, CONF_DEVICE_PATH
|
||||
from zigpy.exceptions import NetworkSettingsInconsistent, TransientConnectionError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_TYPE, EVENT_HOMEASSISTANT_STOP, Platform
|
||||
from homeassistant.core import Event, HomeAssistant
|
||||
from homeassistant.const import (
|
||||
CONF_TYPE,
|
||||
EVENT_CORE_CONFIG_UPDATE,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
@@ -204,6 +210,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
|
||||
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_shutdown)
|
||||
)
|
||||
|
||||
@callback
|
||||
def update_config(event: Event) -> None:
|
||||
"""Handle Core config update."""
|
||||
zha_gateway.config.local_timezone = ZoneInfo(hass.config.time_zone)
|
||||
|
||||
config_entry.async_on_unload(
|
||||
hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, update_config)
|
||||
)
|
||||
|
||||
await ha_zha_data.gateway_proxy.async_initialize_devices_and_entities()
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
async_dispatcher_send(hass, SIGNAL_ADD_ENTITIES)
|
||||
|
||||
@@ -62,7 +62,7 @@ class ZHAEntity(LogMixin, RestoreEntity, Entity):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return entity availability."""
|
||||
return self.entity_data.device_proxy.device.available
|
||||
return self.entity_data.entity.available
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo:
|
||||
|
||||
@@ -15,6 +15,7 @@ import re
|
||||
import time
|
||||
from types import MappingProxyType
|
||||
from typing import TYPE_CHECKING, Any, Concatenate, NamedTuple, ParamSpec, TypeVar, cast
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import voluptuous as vol
|
||||
from zha.application.const import (
|
||||
@@ -1273,6 +1274,7 @@ def create_zha_config(hass: HomeAssistant, ha_zha_data: HAZHAData) -> ZHAData:
|
||||
quirks_configuration=quirks_config,
|
||||
device_overrides=overrides_config,
|
||||
),
|
||||
local_timezone=ZoneInfo(hass.config.time_zone),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"zha",
|
||||
"universal_silabs_flasher"
|
||||
],
|
||||
"requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.28"],
|
||||
"requirements": ["universal-silabs-flasher==0.0.22", "zha==0.0.31"],
|
||||
"usb": [
|
||||
{
|
||||
"vid": "10C4",
|
||||
|
||||
+3
-11
@@ -817,9 +817,7 @@ async def async_process_ha_core_config(hass: HomeAssistant, config: dict) -> Non
|
||||
|
||||
This method is a coroutine.
|
||||
"""
|
||||
# CORE_CONFIG_SCHEMA is not async safe since it uses vol.IsDir
|
||||
# so we need to run it in an executor job.
|
||||
config = await hass.async_add_executor_job(CORE_CONFIG_SCHEMA, config)
|
||||
config = CORE_CONFIG_SCHEMA(config)
|
||||
|
||||
# Only load auth during startup.
|
||||
if not hasattr(hass, "auth"):
|
||||
@@ -1535,15 +1533,9 @@ async def async_process_component_config(
|
||||
return IntegrationConfigInfo(None, config_exceptions)
|
||||
|
||||
# No custom config validator, proceed with schema validation
|
||||
if config_schema := getattr(component, "CONFIG_SCHEMA", None):
|
||||
if hasattr(component, "CONFIG_SCHEMA"):
|
||||
try:
|
||||
if domain in config:
|
||||
# cv.isdir, cv.isfile, cv.isdevice are not async
|
||||
# friendly so we need to run this in executor
|
||||
schema = await hass.async_add_executor_job(config_schema, config)
|
||||
else:
|
||||
schema = config_schema(config)
|
||||
return IntegrationConfigInfo(schema, [])
|
||||
return IntegrationConfigInfo(component.CONFIG_SCHEMA(config), [])
|
||||
except vol.Invalid as exc:
|
||||
exc_info = ConfigExceptionInfo(
|
||||
exc,
|
||||
|
||||
@@ -24,7 +24,7 @@ if TYPE_CHECKING:
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2024
|
||||
MINOR_VERSION: Final = 8
|
||||
PATCH_VERSION: Final = "0"
|
||||
PATCH_VERSION: Final = "2"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0)
|
||||
|
||||
@@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.2
|
||||
aiodiscover==2.1.0
|
||||
aiodns==3.2.0
|
||||
aiohttp-fast-zlib==0.1.1
|
||||
aiohttp==3.10.1
|
||||
aiohttp==3.10.3
|
||||
aiohttp_cors==0.7.0
|
||||
aiozoneinfo==0.2.1
|
||||
astral==2.2
|
||||
@@ -16,7 +16,7 @@ awesomeversion==24.6.0
|
||||
bcrypt==4.1.3
|
||||
bleak-retry-connector==3.5.0
|
||||
bleak==0.22.2
|
||||
bluetooth-adapters==0.19.3
|
||||
bluetooth-adapters==0.19.4
|
||||
bluetooth-auto-recovery==1.4.2
|
||||
bluetooth-data-tools==1.19.4
|
||||
cached_ipaddress==0.3.0
|
||||
@@ -31,7 +31,7 @@ habluetooth==3.1.3
|
||||
hass-nabucasa==0.81.1
|
||||
hassil==1.7.4
|
||||
home-assistant-bluetooth==1.12.2
|
||||
home-assistant-frontend==20240806.1
|
||||
home-assistant-frontend==20240809.0
|
||||
home-assistant-intents==2024.8.7
|
||||
httpx==0.27.0
|
||||
ifaddr==0.2.0
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2024.8.0"
|
||||
version = "2024.8.2"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
@@ -24,7 +24,7 @@ classifiers = [
|
||||
requires-python = ">=3.12.0"
|
||||
dependencies = [
|
||||
"aiodns==3.2.0",
|
||||
"aiohttp==3.10.1",
|
||||
"aiohttp==3.10.3",
|
||||
"aiohttp_cors==0.7.0",
|
||||
"aiohttp-fast-zlib==0.1.1",
|
||||
"aiozoneinfo==0.2.1",
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
|
||||
# Home Assistant Core
|
||||
aiodns==3.2.0
|
||||
aiohttp==3.10.1
|
||||
aiohttp==3.10.3
|
||||
aiohttp_cors==0.7.0
|
||||
aiohttp-fast-zlib==0.1.1
|
||||
aiozoneinfo==0.2.1
|
||||
|
||||
+39
-33
@@ -4,7 +4,7 @@
|
||||
-r requirements.txt
|
||||
|
||||
# homeassistant.components.aemet
|
||||
AEMET-OpenData==0.5.3
|
||||
AEMET-OpenData==0.5.4
|
||||
|
||||
# homeassistant.components.honeywell
|
||||
AIOSomecomfort==0.0.25
|
||||
@@ -176,7 +176,7 @@ aio-georss-gdacs==0.9
|
||||
aioairq==0.3.2
|
||||
|
||||
# homeassistant.components.airzone_cloud
|
||||
aioairzone-cloud==0.6.1
|
||||
aioairzone-cloud==0.6.2
|
||||
|
||||
# homeassistant.components.airzone
|
||||
aioairzone==0.8.1
|
||||
@@ -255,7 +255,7 @@ aioguardian==2022.07.0
|
||||
aioharmony==0.2.10
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==3.2.1
|
||||
aiohomekit==3.2.2
|
||||
|
||||
# homeassistant.components.hue
|
||||
aiohue==4.7.2
|
||||
@@ -288,7 +288,7 @@ aiolookin==1.0.0
|
||||
aiolyric==1.1.0
|
||||
|
||||
# homeassistant.components.mealie
|
||||
aiomealie==0.8.0
|
||||
aiomealie==0.8.1
|
||||
|
||||
# homeassistant.components.modern_forms
|
||||
aiomodernforms==0.1.8
|
||||
@@ -335,7 +335,7 @@ aiopvpc==4.2.2
|
||||
aiopyarr==23.4.0
|
||||
|
||||
# homeassistant.components.qnap_qsw
|
||||
aioqsw==0.4.0
|
||||
aioqsw==0.4.1
|
||||
|
||||
# homeassistant.components.rainforest_raven
|
||||
aioraven==0.7.0
|
||||
@@ -350,7 +350,7 @@ aioridwell==2024.01.0
|
||||
aioruckus==0.34
|
||||
|
||||
# homeassistant.components.russound_rio
|
||||
aiorussound==2.2.0
|
||||
aiorussound==2.2.3
|
||||
|
||||
# homeassistant.components.ruuvi_gateway
|
||||
aioruuvigateway==0.1.0
|
||||
@@ -359,7 +359,7 @@ aioruuvigateway==0.1.0
|
||||
aiosenz==1.0.0
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==11.1.0
|
||||
aioshelly==11.2.0
|
||||
|
||||
# homeassistant.components.skybell
|
||||
aioskybell==22.7.0
|
||||
@@ -386,7 +386,7 @@ aiotankerkoenig==0.4.1
|
||||
aiotractive==0.6.0
|
||||
|
||||
# homeassistant.components.unifi
|
||||
aiounifi==79
|
||||
aiounifi==80
|
||||
|
||||
# homeassistant.components.vlc_telnet
|
||||
aiovlc==0.3.2
|
||||
@@ -410,7 +410,7 @@ aiowithings==3.0.2
|
||||
aioymaps==1.2.5
|
||||
|
||||
# homeassistant.components.airgradient
|
||||
airgradient==0.7.1
|
||||
airgradient==0.8.0
|
||||
|
||||
# homeassistant.components.airly
|
||||
airly==1.1.0
|
||||
@@ -591,7 +591,7 @@ bluemaestro-ble==0.2.3
|
||||
# bluepy==1.3.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-adapters==0.19.3
|
||||
bluetooth-adapters==0.19.4
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-auto-recovery==1.4.2
|
||||
@@ -660,6 +660,9 @@ clearpasspy==1.0.2
|
||||
# homeassistant.components.sinch
|
||||
clx-sdk-xms==1.0.0
|
||||
|
||||
# homeassistant.components.coinbase
|
||||
coinbase-advanced-py==1.2.2
|
||||
|
||||
# homeassistant.components.coinbase
|
||||
coinbase==2.1.0
|
||||
|
||||
@@ -669,6 +672,9 @@ colorlog==6.8.2
|
||||
# homeassistant.components.color_extractor
|
||||
colorthief==0.2.1
|
||||
|
||||
# homeassistant.components.concord232
|
||||
concord232==0.15.1
|
||||
|
||||
# homeassistant.components.upc_connect
|
||||
connect-box==0.3.1
|
||||
|
||||
@@ -732,7 +738,7 @@ devolo-home-control-api==0.18.3
|
||||
devolo-plc-api==1.4.1
|
||||
|
||||
# homeassistant.components.chacon_dio
|
||||
dio-chacon-wifi-api==1.1.0
|
||||
dio-chacon-wifi-api==1.2.0
|
||||
|
||||
# homeassistant.components.directv
|
||||
directv==0.4.0
|
||||
@@ -983,7 +989,7 @@ google-cloud-texttospeech==2.16.3
|
||||
google-generativeai==0.6.0
|
||||
|
||||
# homeassistant.components.nest
|
||||
google-nest-sdm==4.0.5
|
||||
google-nest-sdm==4.0.6
|
||||
|
||||
# homeassistant.components.google_travel_time
|
||||
googlemaps==2.5.1
|
||||
@@ -1093,7 +1099,7 @@ hole==0.8.0
|
||||
holidays==0.53
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20240806.1
|
||||
home-assistant-frontend==20240809.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2024.8.7
|
||||
@@ -1216,7 +1222,7 @@ kiwiki-client==0.1.1
|
||||
knocki==0.3.1
|
||||
|
||||
# homeassistant.components.knx
|
||||
knx-frontend==2024.8.6.211307
|
||||
knx-frontend==2024.8.9.225351
|
||||
|
||||
# homeassistant.components.konnected
|
||||
konnected==1.2.0
|
||||
@@ -1225,7 +1231,7 @@ konnected==1.2.0
|
||||
krakenex==2.1.0
|
||||
|
||||
# homeassistant.components.lacrosse_view
|
||||
lacrosse-view==1.0.1
|
||||
lacrosse-view==1.0.2
|
||||
|
||||
# homeassistant.components.eufy
|
||||
lakeside==0.13
|
||||
@@ -1351,7 +1357,7 @@ moat-ble==0.1.1
|
||||
moehlenhoff-alpha2==1.3.1
|
||||
|
||||
# homeassistant.components.monzo
|
||||
monzopy==1.3.0
|
||||
monzopy==1.3.2
|
||||
|
||||
# homeassistant.components.mopeka
|
||||
mopeka-iot-ble==0.8.0
|
||||
@@ -1502,7 +1508,7 @@ openhomedevice==2.2.0
|
||||
opensensemap-api==0.2.0
|
||||
|
||||
# homeassistant.components.enigma2
|
||||
openwebifpy==4.2.5
|
||||
openwebifpy==4.2.7
|
||||
|
||||
# homeassistant.components.luci
|
||||
openwrt-luci-rpc==1.1.17
|
||||
@@ -1650,7 +1656,7 @@ py-madvr2==1.6.29
|
||||
py-melissa-climate==2.1.4
|
||||
|
||||
# homeassistant.components.nextbus
|
||||
py-nextbusnext==2.0.3
|
||||
py-nextbusnext==2.0.4
|
||||
|
||||
# homeassistant.components.nightscout
|
||||
py-nightscout==1.2.2
|
||||
@@ -1662,7 +1668,7 @@ py-schluter==0.1.7
|
||||
py-sucks==0.9.10
|
||||
|
||||
# homeassistant.components.synology_dsm
|
||||
py-synologydsm-api==2.4.4
|
||||
py-synologydsm-api==2.4.5
|
||||
|
||||
# homeassistant.components.zabbix
|
||||
py-zabbix==1.1.7
|
||||
@@ -1786,7 +1792,7 @@ pycsspeechtts==1.0.8
|
||||
# pycups==1.9.73
|
||||
|
||||
# homeassistant.components.daikin
|
||||
pydaikin==2.13.1
|
||||
pydaikin==2.13.4
|
||||
|
||||
# homeassistant.components.danfoss_air
|
||||
pydanfossair==0.1.0
|
||||
@@ -1906,7 +1912,7 @@ pyhiveapi==0.5.16
|
||||
pyhomematic==0.1.77
|
||||
|
||||
# homeassistant.components.homeworks
|
||||
pyhomeworks==1.1.0
|
||||
pyhomeworks==1.1.1
|
||||
|
||||
# homeassistant.components.ialarm
|
||||
pyialarm==2.2.0
|
||||
@@ -1942,7 +1948,7 @@ pyisy==3.1.14
|
||||
pyitachip2ir==0.0.7
|
||||
|
||||
# homeassistant.components.jvc_projector
|
||||
pyjvcprojector==1.0.11
|
||||
pyjvcprojector==1.0.12
|
||||
|
||||
# homeassistant.components.kaleidescape
|
||||
pykaleidescape==1.0.1
|
||||
@@ -1990,7 +1996,7 @@ pylitejet==0.6.2
|
||||
pylitterbot==2023.5.0
|
||||
|
||||
# homeassistant.components.lutron_caseta
|
||||
pylutron-caseta==0.20.0
|
||||
pylutron-caseta==0.21.1
|
||||
|
||||
# homeassistant.components.lutron
|
||||
pylutron==0.2.15
|
||||
@@ -2068,7 +2074,7 @@ pyombi==0.1.10
|
||||
pyopenuv==2023.02.0
|
||||
|
||||
# homeassistant.components.openweathermap
|
||||
pyopenweathermap==0.0.9
|
||||
pyopenweathermap==0.1.1
|
||||
|
||||
# homeassistant.components.opnsense
|
||||
pyopnsense==0.4.0
|
||||
@@ -2097,7 +2103,7 @@ pyownet==0.10.0.post1
|
||||
pypca==0.0.7
|
||||
|
||||
# homeassistant.components.lcn
|
||||
pypck==0.7.17
|
||||
pypck==0.7.20
|
||||
|
||||
# homeassistant.components.pjlink
|
||||
pypjlink2==1.2.1
|
||||
@@ -2157,7 +2163,7 @@ pysabnzbd==1.1.1
|
||||
pysaj==0.0.16
|
||||
|
||||
# homeassistant.components.schlage
|
||||
pyschlage==2024.6.0
|
||||
pyschlage==2024.8.0
|
||||
|
||||
# homeassistant.components.sensibo
|
||||
pysensibo==1.0.36
|
||||
@@ -2280,7 +2286,7 @@ python-gitlab==1.6.0
|
||||
python-homeassistant-analytics==0.7.0
|
||||
|
||||
# homeassistant.components.homewizard
|
||||
python-homewizard-energy==v6.1.1
|
||||
python-homewizard-energy==v6.2.0
|
||||
|
||||
# homeassistant.components.hp_ilo
|
||||
python-hpilo==4.4.3
|
||||
@@ -2700,10 +2706,10 @@ switchbot-api==2.2.1
|
||||
synology-srm==0.2.0
|
||||
|
||||
# homeassistant.components.system_bridge
|
||||
systembridgeconnector==4.1.0
|
||||
systembridgeconnector==4.1.5
|
||||
|
||||
# homeassistant.components.system_bridge
|
||||
systembridgemodels==4.1.0
|
||||
systembridgemodels==4.2.4
|
||||
|
||||
# homeassistant.components.tailscale
|
||||
tailscale==0.6.1
|
||||
@@ -2915,7 +2921,7 @@ wiffi==1.1.2
|
||||
wirelesstagpy==0.8.1
|
||||
|
||||
# homeassistant.components.wled
|
||||
wled==0.20.1
|
||||
wled==0.20.2
|
||||
|
||||
# homeassistant.components.wolflink
|
||||
wolf-comm==0.0.9
|
||||
@@ -2930,7 +2936,7 @@ xbox-webapi==2.0.11
|
||||
xiaomi-ble==0.30.2
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==3.0.0
|
||||
xknx==3.1.0
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknxproject==3.7.1
|
||||
@@ -2962,7 +2968,7 @@ yeelight==0.7.14
|
||||
yeelightsunflower==0.0.10
|
||||
|
||||
# homeassistant.components.yolink
|
||||
yolink-api==0.4.6
|
||||
yolink-api==0.4.7
|
||||
|
||||
# homeassistant.components.youless
|
||||
youless-api==2.1.2
|
||||
@@ -2986,7 +2992,7 @@ zeroconf==0.132.2
|
||||
zeversolar==0.3.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha==0.0.28
|
||||
zha==0.0.31
|
||||
|
||||
# homeassistant.components.zhong_hong
|
||||
zhong-hong-hvac==1.0.12
|
||||
|
||||
+36
-33
@@ -4,7 +4,7 @@
|
||||
-r requirements_test.txt
|
||||
|
||||
# homeassistant.components.aemet
|
||||
AEMET-OpenData==0.5.3
|
||||
AEMET-OpenData==0.5.4
|
||||
|
||||
# homeassistant.components.honeywell
|
||||
AIOSomecomfort==0.0.25
|
||||
@@ -164,7 +164,7 @@ aio-georss-gdacs==0.9
|
||||
aioairq==0.3.2
|
||||
|
||||
# homeassistant.components.airzone_cloud
|
||||
aioairzone-cloud==0.6.1
|
||||
aioairzone-cloud==0.6.2
|
||||
|
||||
# homeassistant.components.airzone
|
||||
aioairzone==0.8.1
|
||||
@@ -240,7 +240,7 @@ aioguardian==2022.07.0
|
||||
aioharmony==0.2.10
|
||||
|
||||
# homeassistant.components.homekit_controller
|
||||
aiohomekit==3.2.1
|
||||
aiohomekit==3.2.2
|
||||
|
||||
# homeassistant.components.hue
|
||||
aiohue==4.7.2
|
||||
@@ -270,7 +270,7 @@ aiolookin==1.0.0
|
||||
aiolyric==1.1.0
|
||||
|
||||
# homeassistant.components.mealie
|
||||
aiomealie==0.8.0
|
||||
aiomealie==0.8.1
|
||||
|
||||
# homeassistant.components.modern_forms
|
||||
aiomodernforms==0.1.8
|
||||
@@ -317,7 +317,7 @@ aiopvpc==4.2.2
|
||||
aiopyarr==23.4.0
|
||||
|
||||
# homeassistant.components.qnap_qsw
|
||||
aioqsw==0.4.0
|
||||
aioqsw==0.4.1
|
||||
|
||||
# homeassistant.components.rainforest_raven
|
||||
aioraven==0.7.0
|
||||
@@ -332,7 +332,7 @@ aioridwell==2024.01.0
|
||||
aioruckus==0.34
|
||||
|
||||
# homeassistant.components.russound_rio
|
||||
aiorussound==2.2.0
|
||||
aiorussound==2.2.3
|
||||
|
||||
# homeassistant.components.ruuvi_gateway
|
||||
aioruuvigateway==0.1.0
|
||||
@@ -341,7 +341,7 @@ aioruuvigateway==0.1.0
|
||||
aiosenz==1.0.0
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==11.1.0
|
||||
aioshelly==11.2.0
|
||||
|
||||
# homeassistant.components.skybell
|
||||
aioskybell==22.7.0
|
||||
@@ -368,7 +368,7 @@ aiotankerkoenig==0.4.1
|
||||
aiotractive==0.6.0
|
||||
|
||||
# homeassistant.components.unifi
|
||||
aiounifi==79
|
||||
aiounifi==80
|
||||
|
||||
# homeassistant.components.vlc_telnet
|
||||
aiovlc==0.3.2
|
||||
@@ -392,7 +392,7 @@ aiowithings==3.0.2
|
||||
aioymaps==1.2.5
|
||||
|
||||
# homeassistant.components.airgradient
|
||||
airgradient==0.7.1
|
||||
airgradient==0.8.0
|
||||
|
||||
# homeassistant.components.airly
|
||||
airly==1.1.0
|
||||
@@ -515,7 +515,7 @@ bluecurrent-api==1.2.3
|
||||
bluemaestro-ble==0.2.3
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-adapters==0.19.3
|
||||
bluetooth-adapters==0.19.4
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bluetooth-auto-recovery==1.4.2
|
||||
@@ -562,6 +562,9 @@ cached_ipaddress==0.3.0
|
||||
# homeassistant.components.caldav
|
||||
caldav==1.3.9
|
||||
|
||||
# homeassistant.components.coinbase
|
||||
coinbase-advanced-py==1.2.2
|
||||
|
||||
# homeassistant.components.coinbase
|
||||
coinbase==2.1.0
|
||||
|
||||
@@ -625,7 +628,7 @@ devolo-home-control-api==0.18.3
|
||||
devolo-plc-api==1.4.1
|
||||
|
||||
# homeassistant.components.chacon_dio
|
||||
dio-chacon-wifi-api==1.1.0
|
||||
dio-chacon-wifi-api==1.2.0
|
||||
|
||||
# homeassistant.components.directv
|
||||
directv==0.4.0
|
||||
@@ -830,7 +833,7 @@ google-cloud-pubsub==2.13.11
|
||||
google-generativeai==0.6.0
|
||||
|
||||
# homeassistant.components.nest
|
||||
google-nest-sdm==4.0.5
|
||||
google-nest-sdm==4.0.6
|
||||
|
||||
# homeassistant.components.google_travel_time
|
||||
googlemaps==2.5.1
|
||||
@@ -916,7 +919,7 @@ hole==0.8.0
|
||||
holidays==0.53
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20240806.1
|
||||
home-assistant-frontend==20240809.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2024.8.7
|
||||
@@ -1012,7 +1015,7 @@ kegtron-ble==0.4.0
|
||||
knocki==0.3.1
|
||||
|
||||
# homeassistant.components.knx
|
||||
knx-frontend==2024.8.6.211307
|
||||
knx-frontend==2024.8.9.225351
|
||||
|
||||
# homeassistant.components.konnected
|
||||
konnected==1.2.0
|
||||
@@ -1021,7 +1024,7 @@ konnected==1.2.0
|
||||
krakenex==2.1.0
|
||||
|
||||
# homeassistant.components.lacrosse_view
|
||||
lacrosse-view==1.0.1
|
||||
lacrosse-view==1.0.2
|
||||
|
||||
# homeassistant.components.laundrify
|
||||
laundrify-aio==1.2.2
|
||||
@@ -1117,7 +1120,7 @@ moat-ble==0.1.1
|
||||
moehlenhoff-alpha2==1.3.1
|
||||
|
||||
# homeassistant.components.monzo
|
||||
monzopy==1.3.0
|
||||
monzopy==1.3.2
|
||||
|
||||
# homeassistant.components.mopeka
|
||||
mopeka-iot-ble==0.8.0
|
||||
@@ -1235,7 +1238,7 @@ openerz-api==0.3.0
|
||||
openhomedevice==2.2.0
|
||||
|
||||
# homeassistant.components.enigma2
|
||||
openwebifpy==4.2.5
|
||||
openwebifpy==4.2.7
|
||||
|
||||
# homeassistant.components.opower
|
||||
opower==0.6.0
|
||||
@@ -1342,7 +1345,7 @@ py-madvr2==1.6.29
|
||||
py-melissa-climate==2.1.4
|
||||
|
||||
# homeassistant.components.nextbus
|
||||
py-nextbusnext==2.0.3
|
||||
py-nextbusnext==2.0.4
|
||||
|
||||
# homeassistant.components.nightscout
|
||||
py-nightscout==1.2.2
|
||||
@@ -1351,7 +1354,7 @@ py-nightscout==1.2.2
|
||||
py-sucks==0.9.10
|
||||
|
||||
# homeassistant.components.synology_dsm
|
||||
py-synologydsm-api==2.4.4
|
||||
py-synologydsm-api==2.4.5
|
||||
|
||||
# homeassistant.components.hdmi_cec
|
||||
pyCEC==0.5.2
|
||||
@@ -1433,7 +1436,7 @@ pycoolmasternet-async==0.1.5
|
||||
pycsspeechtts==1.0.8
|
||||
|
||||
# homeassistant.components.daikin
|
||||
pydaikin==2.13.1
|
||||
pydaikin==2.13.4
|
||||
|
||||
# homeassistant.components.deconz
|
||||
pydeconz==116
|
||||
@@ -1520,7 +1523,7 @@ pyhiveapi==0.5.16
|
||||
pyhomematic==0.1.77
|
||||
|
||||
# homeassistant.components.homeworks
|
||||
pyhomeworks==1.1.0
|
||||
pyhomeworks==1.1.1
|
||||
|
||||
# homeassistant.components.ialarm
|
||||
pyialarm==2.2.0
|
||||
@@ -1547,7 +1550,7 @@ pyiss==1.0.1
|
||||
pyisy==3.1.14
|
||||
|
||||
# homeassistant.components.jvc_projector
|
||||
pyjvcprojector==1.0.11
|
||||
pyjvcprojector==1.0.12
|
||||
|
||||
# homeassistant.components.kaleidescape
|
||||
pykaleidescape==1.0.1
|
||||
@@ -1589,7 +1592,7 @@ pylitejet==0.6.2
|
||||
pylitterbot==2023.5.0
|
||||
|
||||
# homeassistant.components.lutron_caseta
|
||||
pylutron-caseta==0.20.0
|
||||
pylutron-caseta==0.21.1
|
||||
|
||||
# homeassistant.components.lutron
|
||||
pylutron==0.2.15
|
||||
@@ -1652,7 +1655,7 @@ pyoctoprintapi==0.1.12
|
||||
pyopenuv==2023.02.0
|
||||
|
||||
# homeassistant.components.openweathermap
|
||||
pyopenweathermap==0.0.9
|
||||
pyopenweathermap==0.1.1
|
||||
|
||||
# homeassistant.components.opnsense
|
||||
pyopnsense==0.4.0
|
||||
@@ -1675,7 +1678,7 @@ pyoverkiz==1.13.14
|
||||
pyownet==0.10.0.post1
|
||||
|
||||
# homeassistant.components.lcn
|
||||
pypck==0.7.17
|
||||
pypck==0.7.20
|
||||
|
||||
# homeassistant.components.pjlink
|
||||
pypjlink2==1.2.1
|
||||
@@ -1720,7 +1723,7 @@ pyrympro==0.0.8
|
||||
pysabnzbd==1.1.1
|
||||
|
||||
# homeassistant.components.schlage
|
||||
pyschlage==2024.6.0
|
||||
pyschlage==2024.8.0
|
||||
|
||||
# homeassistant.components.sensibo
|
||||
pysensibo==1.0.36
|
||||
@@ -1804,7 +1807,7 @@ python-fullykiosk==0.0.14
|
||||
python-homeassistant-analytics==0.7.0
|
||||
|
||||
# homeassistant.components.homewizard
|
||||
python-homewizard-energy==v6.1.1
|
||||
python-homewizard-energy==v6.2.0
|
||||
|
||||
# homeassistant.components.izone
|
||||
python-izone==1.2.9
|
||||
@@ -2134,10 +2137,10 @@ surepy==0.9.0
|
||||
switchbot-api==2.2.1
|
||||
|
||||
# homeassistant.components.system_bridge
|
||||
systembridgeconnector==4.1.0
|
||||
systembridgeconnector==4.1.5
|
||||
|
||||
# homeassistant.components.system_bridge
|
||||
systembridgemodels==4.1.0
|
||||
systembridgemodels==4.2.4
|
||||
|
||||
# homeassistant.components.tailscale
|
||||
tailscale==0.6.1
|
||||
@@ -2298,7 +2301,7 @@ whois==0.9.27
|
||||
wiffi==1.1.2
|
||||
|
||||
# homeassistant.components.wled
|
||||
wled==0.20.1
|
||||
wled==0.20.2
|
||||
|
||||
# homeassistant.components.wolflink
|
||||
wolf-comm==0.0.9
|
||||
@@ -2313,7 +2316,7 @@ xbox-webapi==2.0.11
|
||||
xiaomi-ble==0.30.2
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknx==3.0.0
|
||||
xknx==3.1.0
|
||||
|
||||
# homeassistant.components.knx
|
||||
xknxproject==3.7.1
|
||||
@@ -2339,7 +2342,7 @@ yalexs==6.4.3
|
||||
yeelight==0.7.14
|
||||
|
||||
# homeassistant.components.yolink
|
||||
yolink-api==0.4.6
|
||||
yolink-api==0.4.7
|
||||
|
||||
# homeassistant.components.youless
|
||||
youless-api==2.1.2
|
||||
@@ -2360,7 +2363,7 @@ zeroconf==0.132.2
|
||||
zeversolar==0.3.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
zha==0.0.28
|
||||
zha==0.0.31
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.57.0
|
||||
|
||||
+1
-1
@@ -124,6 +124,7 @@ EXCEPTIONS = {
|
||||
"PyXiaomiGateway", # https://github.com/Danielhiversen/PyXiaomiGateway/pull/201
|
||||
"aiocomelit", # https://github.com/chemelli74/aiocomelit/pull/138
|
||||
"aioecowitt", # https://github.com/home-assistant-libs/aioecowitt/pull/180
|
||||
"aiohappyeyeballs", # Python-2.0.1
|
||||
"aioopenexchangerates", # https://github.com/MartinHjelmare/aioopenexchangerates/pull/94
|
||||
"aiooui", # https://github.com/Bluetooth-Devices/aiooui/pull/8
|
||||
"aioruuvigateway", # https://github.com/akx/aioruuvigateway/pull/6
|
||||
@@ -159,7 +160,6 @@ EXCEPTIONS = {
|
||||
"pyTibber", # https://github.com/Danielhiversen/pyTibber/pull/294
|
||||
"pybbox", # https://github.com/HydrelioxGitHub/pybbox/pull/5
|
||||
"pyeconet", # https://github.com/w1ll1am23/pyeconet/pull/41
|
||||
"pylutron-caseta", # https://github.com/gurumitts/pylutron-caseta/pull/168
|
||||
"pysabnzbd", # https://github.com/jeradM/pysabnzbd/pull/6
|
||||
"pyvera", # https://github.com/maximvelichko/pyvera/pull/164
|
||||
"pyxeoma", # https://github.com/jeradM/pyxeoma/pull/11
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
from pyblu import SyncStatus
|
||||
from pyblu import Status, SyncStatus
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.bluesound.const import DOMAIN
|
||||
@@ -39,6 +39,35 @@ def sync_status() -> SyncStatus:
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def status() -> Status:
|
||||
"""Return a status object."""
|
||||
return Status(
|
||||
etag="etag",
|
||||
input_id=None,
|
||||
service=None,
|
||||
state="playing",
|
||||
shuffle=False,
|
||||
album=None,
|
||||
artist=None,
|
||||
name=None,
|
||||
image=None,
|
||||
volume=10,
|
||||
volume_db=22.3,
|
||||
mute=False,
|
||||
mute_volume=None,
|
||||
mute_volume_db=None,
|
||||
seconds=2,
|
||||
total_seconds=123.1,
|
||||
can_seek=False,
|
||||
sleep=0,
|
||||
group_name=None,
|
||||
group_volume=None,
|
||||
indexing=False,
|
||||
stream_url=None,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock, None, None]:
|
||||
"""Override async_setup_entry."""
|
||||
@@ -65,7 +94,7 @@ def mock_config_entry(hass: HomeAssistant) -> MockConfigEntry:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_player() -> Generator[AsyncMock]:
|
||||
def mock_player(status: Status) -> Generator[AsyncMock]:
|
||||
"""Mock the player."""
|
||||
with (
|
||||
patch(
|
||||
@@ -78,7 +107,7 @@ def mock_player() -> Generator[AsyncMock]:
|
||||
):
|
||||
player = mock_player.return_value
|
||||
player.__aenter__.return_value = player
|
||||
player.status.return_value = None
|
||||
player.status.return_value = status
|
||||
player.sync_status.return_value = SyncStatus(
|
||||
etag="etag",
|
||||
id="1.1.1.1:11000",
|
||||
|
||||
@@ -41,7 +41,7 @@ async def test_user_flow_success(
|
||||
|
||||
|
||||
async def test_user_flow_cannot_connect(
|
||||
hass: HomeAssistant, mock_player: AsyncMock
|
||||
hass: HomeAssistant, mock_player: AsyncMock, mock_setup_entry: AsyncMock
|
||||
) -> None:
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
@@ -76,6 +76,8 @@ async def test_user_flow_cannot_connect(
|
||||
CONF_PORT: 11000,
|
||||
}
|
||||
|
||||
mock_setup_entry.assert_called_once()
|
||||
|
||||
|
||||
async def test_user_flow_aleady_configured(
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -5,13 +5,14 @@ from homeassistant.components.coinbase.const import (
|
||||
CONF_EXCHANGE_RATES,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN
|
||||
from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, CONF_API_VERSION
|
||||
|
||||
from .const import (
|
||||
GOOD_CURRENCY_2,
|
||||
GOOD_EXCHANGE_RATE,
|
||||
GOOD_EXCHANGE_RATE_2,
|
||||
MOCK_ACCOUNTS_RESPONSE,
|
||||
MOCK_ACCOUNTS_RESPONSE_V3,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
@@ -54,6 +55,33 @@ def mocked_get_accounts(_, **kwargs):
|
||||
return MockGetAccounts(**kwargs)
|
||||
|
||||
|
||||
class MockGetAccountsV3:
|
||||
"""Mock accounts with pagination."""
|
||||
|
||||
def __init__(self, cursor=""):
|
||||
"""Init mocked object, forced to return two at a time."""
|
||||
ids = [account["uuid"] for account in MOCK_ACCOUNTS_RESPONSE_V3]
|
||||
start = ids.index(cursor) if cursor else 0
|
||||
|
||||
has_next = (target_end := start + 2) < len(MOCK_ACCOUNTS_RESPONSE_V3)
|
||||
end = target_end if has_next else -1
|
||||
next_cursor = ids[end] if has_next else ids[-1]
|
||||
self.accounts = {
|
||||
"accounts": MOCK_ACCOUNTS_RESPONSE_V3[start:end],
|
||||
"has_next": has_next,
|
||||
"cursor": next_cursor,
|
||||
}
|
||||
|
||||
def __getitem__(self, item):
|
||||
"""Handle subscript request."""
|
||||
return self.accounts[item]
|
||||
|
||||
|
||||
def mocked_get_accounts_v3(_, **kwargs):
|
||||
"""Return simplified accounts using mock."""
|
||||
return MockGetAccountsV3(**kwargs)
|
||||
|
||||
|
||||
def mock_get_current_user():
|
||||
"""Return a simplified mock user."""
|
||||
return {
|
||||
@@ -74,6 +102,19 @@ def mock_get_exchange_rates():
|
||||
}
|
||||
|
||||
|
||||
def mock_get_portfolios():
|
||||
"""Return a mocked list of Coinbase portfolios."""
|
||||
return {
|
||||
"portfolios": [
|
||||
{
|
||||
"name": "Default",
|
||||
"uuid": "123456",
|
||||
"type": "DEFAULT",
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
async def init_mock_coinbase(hass, currencies=None, rates=None):
|
||||
"""Init Coinbase integration for testing."""
|
||||
config_entry = MockConfigEntry(
|
||||
@@ -93,3 +134,28 @@ async def init_mock_coinbase(hass, currencies=None, rates=None):
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return config_entry
|
||||
|
||||
|
||||
async def init_mock_coinbase_v3(hass, currencies=None, rates=None):
|
||||
"""Init Coinbase integration for testing."""
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
entry_id="080272b77a4f80c41b94d7cdc86fd826",
|
||||
unique_id=None,
|
||||
title="Test User v3",
|
||||
data={
|
||||
CONF_API_KEY: "organizations/123456",
|
||||
CONF_API_TOKEN: "AbCDeF",
|
||||
CONF_API_VERSION: "v3",
|
||||
},
|
||||
options={
|
||||
CONF_CURRENCIES: currencies or [],
|
||||
CONF_EXCHANGE_RATES: rates or [],
|
||||
},
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return config_entry
|
||||
|
||||
@@ -31,3 +31,31 @@ MOCK_ACCOUNTS_RESPONSE = [
|
||||
"type": "fiat",
|
||||
},
|
||||
]
|
||||
|
||||
MOCK_ACCOUNTS_RESPONSE_V3 = [
|
||||
{
|
||||
"uuid": "123456789",
|
||||
"name": "BTC Wallet",
|
||||
"currency": GOOD_CURRENCY,
|
||||
"available_balance": {"value": "0.00001", "currency": GOOD_CURRENCY},
|
||||
"type": "ACCOUNT_TYPE_CRYPTO",
|
||||
"hold": {"value": "0", "currency": GOOD_CURRENCY},
|
||||
},
|
||||
{
|
||||
"uuid": "abcdefg",
|
||||
"name": "BTC Vault",
|
||||
"currency": GOOD_CURRENCY,
|
||||
"available_balance": {"value": "100.00", "currency": GOOD_CURRENCY},
|
||||
"type": "ACCOUNT_TYPE_VAULT",
|
||||
"hold": {"value": "0", "currency": GOOD_CURRENCY},
|
||||
},
|
||||
{
|
||||
"uuid": "987654321",
|
||||
"name": "USD Wallet",
|
||||
"currency": GOOD_CURRENCY_2,
|
||||
"available_balance": {"value": "9.90", "currency": GOOD_CURRENCY_2},
|
||||
"type": "ACCOUNT_TYPE_FIAT",
|
||||
"ready": True,
|
||||
"hold": {"value": "0", "currency": GOOD_CURRENCY_2},
|
||||
},
|
||||
]
|
||||
|
||||
@@ -3,40 +3,25 @@
|
||||
dict({
|
||||
'accounts': list([
|
||||
dict({
|
||||
'balance': dict({
|
||||
'amount': '**REDACTED**',
|
||||
'currency': 'BTC',
|
||||
}),
|
||||
'currency': dict({
|
||||
'code': 'BTC',
|
||||
}),
|
||||
'amount': '**REDACTED**',
|
||||
'currency': 'BTC',
|
||||
'id': '**REDACTED**',
|
||||
'is_vault': False,
|
||||
'name': 'BTC Wallet',
|
||||
'type': 'wallet',
|
||||
}),
|
||||
dict({
|
||||
'balance': dict({
|
||||
'amount': '**REDACTED**',
|
||||
'currency': 'BTC',
|
||||
}),
|
||||
'currency': dict({
|
||||
'code': 'BTC',
|
||||
}),
|
||||
'amount': '**REDACTED**',
|
||||
'currency': 'BTC',
|
||||
'id': '**REDACTED**',
|
||||
'is_vault': True,
|
||||
'name': 'BTC Vault',
|
||||
'type': 'vault',
|
||||
}),
|
||||
dict({
|
||||
'balance': dict({
|
||||
'amount': '**REDACTED**',
|
||||
'currency': 'USD',
|
||||
}),
|
||||
'currency': dict({
|
||||
'code': 'USD',
|
||||
}),
|
||||
'amount': '**REDACTED**',
|
||||
'currency': 'USD',
|
||||
'id': '**REDACTED**',
|
||||
'is_vault': False,
|
||||
'name': 'USD Wallet',
|
||||
'type': 'fiat',
|
||||
}),
|
||||
]),
|
||||
'entry': dict({
|
||||
|
||||
@@ -14,15 +14,18 @@ from homeassistant.components.coinbase.const import (
|
||||
CONF_EXCHANGE_RATES,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN
|
||||
from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, CONF_API_VERSION
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from .common import (
|
||||
init_mock_coinbase,
|
||||
init_mock_coinbase_v3,
|
||||
mock_get_current_user,
|
||||
mock_get_exchange_rates,
|
||||
mock_get_portfolios,
|
||||
mocked_get_accounts,
|
||||
mocked_get_accounts_v3,
|
||||
)
|
||||
from .const import BAD_CURRENCY, BAD_EXCHANGE_RATE, GOOD_CURRENCY, GOOD_EXCHANGE_RATE
|
||||
|
||||
@@ -53,16 +56,17 @@ async def test_form(hass: HomeAssistant) -> None:
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_API_KEY: "123456",
|
||||
CONF_API_TOKEN: "AbCDeF",
|
||||
},
|
||||
{CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == "Test User"
|
||||
assert result2["data"] == {CONF_API_KEY: "123456", CONF_API_TOKEN: "AbCDeF"}
|
||||
assert result2["data"] == {
|
||||
CONF_API_KEY: "123456",
|
||||
CONF_API_TOKEN: "AbCDeF",
|
||||
CONF_API_VERSION: "v2",
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@@ -314,3 +318,77 @@ async def test_option_catch_all_exception(hass: HomeAssistant) -> None:
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_form_v3(hass: HomeAssistant) -> None:
|
||||
"""Test we get the form."""
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
with (
|
||||
patch("coinbase.rest.RESTClient.get_accounts", new=mocked_get_accounts_v3),
|
||||
patch(
|
||||
"coinbase.rest.RESTClient.get_portfolios",
|
||||
return_value=mock_get_portfolios(),
|
||||
),
|
||||
patch(
|
||||
"coinbase.rest.RESTBase.get",
|
||||
return_value={"data": mock_get_exchange_rates()},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.coinbase.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{CONF_API_KEY: "organizations/123456", CONF_API_TOKEN: "AbCDeF"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == "Default"
|
||||
assert result2["data"] == {
|
||||
CONF_API_KEY: "organizations/123456",
|
||||
CONF_API_TOKEN: "AbCDeF",
|
||||
CONF_API_VERSION: "v3",
|
||||
}
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_option_form_v3(hass: HomeAssistant) -> None:
|
||||
"""Test we handle a good wallet currency option."""
|
||||
|
||||
with (
|
||||
patch("coinbase.rest.RESTClient.get_accounts", new=mocked_get_accounts_v3),
|
||||
patch(
|
||||
"coinbase.rest.RESTClient.get_portfolios",
|
||||
return_value=mock_get_portfolios(),
|
||||
),
|
||||
patch(
|
||||
"coinbase.rest.RESTBase.get",
|
||||
return_value={"data": mock_get_exchange_rates()},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.coinbase.update_listener"
|
||||
) as mock_update_listener,
|
||||
):
|
||||
config_entry = await init_mock_coinbase_v3(hass)
|
||||
await hass.async_block_till_done()
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
result2 = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={
|
||||
CONF_CURRENCIES: [GOOD_CURRENCY],
|
||||
CONF_EXCHANGE_RATES: [GOOD_EXCHANGE_RATE],
|
||||
CONF_EXCHANGE_PRECISION: 5,
|
||||
},
|
||||
)
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
await hass.async_block_till_done()
|
||||
assert len(mock_update_listener.mock_calls) == 1
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
"1": {
|
||||
"title": "Home Assistant (mydoorbird_motion)",
|
||||
"value": "http://127.0.0.1:8123/api/doorbird/mydoorbird_motion?token=01J2F4B97Y7P1SARXEJ6W07EKD"
|
||||
},
|
||||
"2": {
|
||||
"title": "externally added event",
|
||||
"value": "http://127.0.0.1/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,4 +49,4 @@ async def test_reset_favorites_button(
|
||||
DOMAIN, SERVICE_PRESS, {ATTR_ENTITY_ID: reset_entity_id}, blocking=True
|
||||
)
|
||||
assert hass.states.get(reset_entity_id).state != STATE_UNKNOWN
|
||||
assert doorbird_entry.api.delete_favorite.call_count == 2
|
||||
assert doorbird_entry.api.delete_favorite.call_count == 3
|
||||
|
||||
@@ -219,6 +219,101 @@ async def test_migrate_hourly_gas_to_mbus(
|
||||
)
|
||||
|
||||
|
||||
async def test_migrate_gas_with_devid_to_mbus(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
dsmr_connection_fixture: tuple[MagicMock, MagicMock, MagicMock],
|
||||
) -> None:
|
||||
"""Test migration of unique_id."""
|
||||
(connection_factory, transport, protocol) = dsmr_connection_fixture
|
||||
|
||||
mock_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="/dev/ttyUSB0",
|
||||
data={
|
||||
"port": "/dev/ttyUSB0",
|
||||
"dsmr_version": "5B",
|
||||
"serial_id": "1234",
|
||||
"serial_id_gas": "37464C4F32313139303333373331",
|
||||
},
|
||||
options={
|
||||
"time_between_update": 0,
|
||||
},
|
||||
)
|
||||
|
||||
mock_entry.add_to_hass(hass)
|
||||
|
||||
old_unique_id = "37464C4F32313139303333373331_belgium_5min_gas_meter_reading"
|
||||
|
||||
device = device_registry.async_get_or_create(
|
||||
config_entry_id=mock_entry.entry_id,
|
||||
identifiers={(DOMAIN, "37464C4F32313139303333373331")},
|
||||
name="Gas Meter",
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
entity: er.RegistryEntry = entity_registry.async_get_or_create(
|
||||
suggested_object_id="gas_meter_reading",
|
||||
disabled_by=None,
|
||||
domain=SENSOR_DOMAIN,
|
||||
platform=DOMAIN,
|
||||
device_id=device.id,
|
||||
unique_id=old_unique_id,
|
||||
config_entry=mock_entry,
|
||||
)
|
||||
assert entity.unique_id == old_unique_id
|
||||
await hass.async_block_till_done()
|
||||
|
||||
telegram = Telegram()
|
||||
telegram.add(
|
||||
MBUS_DEVICE_TYPE,
|
||||
CosemObject((0, 1), [{"value": "003", "unit": ""}]),
|
||||
"MBUS_DEVICE_TYPE",
|
||||
)
|
||||
telegram.add(
|
||||
MBUS_EQUIPMENT_IDENTIFIER,
|
||||
CosemObject(
|
||||
(0, 1),
|
||||
[{"value": "37464C4F32313139303333373331", "unit": ""}],
|
||||
),
|
||||
"MBUS_EQUIPMENT_IDENTIFIER",
|
||||
)
|
||||
telegram.add(
|
||||
MBUS_METER_READING,
|
||||
MBusObject(
|
||||
(0, 1),
|
||||
[
|
||||
{"value": datetime.datetime.fromtimestamp(1551642213)},
|
||||
{"value": Decimal(745.695), "unit": "m3"},
|
||||
],
|
||||
),
|
||||
"MBUS_METER_READING",
|
||||
)
|
||||
|
||||
assert await hass.config_entries.async_setup(mock_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
telegram_callback = connection_factory.call_args_list[0][0][2]
|
||||
|
||||
# simulate a telegram pushed from the smartmeter and parsed by dsmr_parser
|
||||
telegram_callback(telegram)
|
||||
|
||||
# after receiving telegram entities need to have the chance to be created
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert (
|
||||
entity_registry.async_get_entity_id(SENSOR_DOMAIN, DOMAIN, old_unique_id)
|
||||
is None
|
||||
)
|
||||
assert (
|
||||
entity_registry.async_get_entity_id(
|
||||
SENSOR_DOMAIN, DOMAIN, "37464C4F32313139303333373331"
|
||||
)
|
||||
== "sensor.gas_meter_reading"
|
||||
)
|
||||
|
||||
|
||||
async def test_migrate_gas_to_mbus_exists(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"""Common fixture for Environment Canada tests."""
|
||||
|
||||
import contextlib
|
||||
from datetime import datetime
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.common import load_fixture
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ec_data():
|
||||
"""Load Environment Canada data."""
|
||||
|
||||
def date_hook(weather):
|
||||
"""Convert timestamp string to datetime."""
|
||||
|
||||
if t := weather.get("timestamp"):
|
||||
with contextlib.suppress(ValueError):
|
||||
weather["timestamp"] = datetime.fromisoformat(t)
|
||||
return weather
|
||||
|
||||
return json.loads(
|
||||
load_fixture("environment_canada/current_conditions_data.json"),
|
||||
object_hook=date_hook,
|
||||
)
|
||||
@@ -5,35 +5,35 @@
|
||||
'forecast': list([
|
||||
dict({
|
||||
'condition': 'sunny',
|
||||
'datetime': '2022-10-04 15:00:00+00:00',
|
||||
'datetime': '2022-10-04T15:00:00+00:00',
|
||||
'precipitation_probability': 0,
|
||||
'temperature': 18.0,
|
||||
'templow': 3.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'sunny',
|
||||
'datetime': '2022-10-05 15:00:00+00:00',
|
||||
'datetime': '2022-10-05T15:00:00+00:00',
|
||||
'precipitation_probability': 0,
|
||||
'temperature': 20.0,
|
||||
'templow': 9.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2022-10-06 15:00:00+00:00',
|
||||
'datetime': '2022-10-06T15:00:00+00:00',
|
||||
'precipitation_probability': 0,
|
||||
'temperature': 20.0,
|
||||
'templow': 7.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'rainy',
|
||||
'datetime': '2022-10-07 15:00:00+00:00',
|
||||
'datetime': '2022-10-07T15:00:00+00:00',
|
||||
'precipitation_probability': 40,
|
||||
'temperature': 13.0,
|
||||
'templow': 1.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2022-10-08 15:00:00+00:00',
|
||||
'datetime': '2022-10-08T15:00:00+00:00',
|
||||
'precipitation_probability': 0,
|
||||
'temperature': 10.0,
|
||||
'templow': 3.0,
|
||||
@@ -48,42 +48,42 @@
|
||||
'forecast': list([
|
||||
dict({
|
||||
'condition': 'clear-night',
|
||||
'datetime': '2022-10-03 15:00:00+00:00',
|
||||
'datetime': '2022-10-03T15:00:00+00:00',
|
||||
'precipitation_probability': 0,
|
||||
'temperature': None,
|
||||
'templow': -1.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'sunny',
|
||||
'datetime': '2022-10-04 15:00:00+00:00',
|
||||
'datetime': '2022-10-04T15:00:00+00:00',
|
||||
'precipitation_probability': 0,
|
||||
'temperature': 18.0,
|
||||
'templow': 3.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'sunny',
|
||||
'datetime': '2022-10-05 15:00:00+00:00',
|
||||
'datetime': '2022-10-05T15:00:00+00:00',
|
||||
'precipitation_probability': 0,
|
||||
'temperature': 20.0,
|
||||
'templow': 9.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2022-10-06 15:00:00+00:00',
|
||||
'datetime': '2022-10-06T15:00:00+00:00',
|
||||
'precipitation_probability': 0,
|
||||
'temperature': 20.0,
|
||||
'templow': 7.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'rainy',
|
||||
'datetime': '2022-10-07 15:00:00+00:00',
|
||||
'datetime': '2022-10-07T15:00:00+00:00',
|
||||
'precipitation_probability': 40,
|
||||
'temperature': 13.0,
|
||||
'templow': 1.0,
|
||||
}),
|
||||
dict({
|
||||
'condition': 'partlycloudy',
|
||||
'datetime': '2022-10-08 15:00:00+00:00',
|
||||
'datetime': '2022-10-08T15:00:00+00:00',
|
||||
'precipitation_probability': 0,
|
||||
'temperature': 10.0,
|
||||
'templow': 3.0,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Test Environment Canada diagnostics."""
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from syrupy import SnapshotAssertion
|
||||
|
||||
@@ -26,6 +27,7 @@ async def test_entry_diagnostics(
|
||||
hass: HomeAssistant,
|
||||
hass_client: ClientSessionGenerator,
|
||||
snapshot: SnapshotAssertion,
|
||||
ec_data: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test config entry diagnostics."""
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Test weather."""
|
||||
|
||||
import json
|
||||
import copy
|
||||
from typing import Any
|
||||
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
@@ -12,23 +13,17 @@ from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import init_integration
|
||||
|
||||
from tests.common import load_fixture
|
||||
|
||||
|
||||
async def test_forecast_daily(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
hass: HomeAssistant, snapshot: SnapshotAssertion, ec_data: dict[str, Any]
|
||||
) -> None:
|
||||
"""Test basic forecast."""
|
||||
|
||||
ec_data = json.loads(
|
||||
load_fixture("environment_canada/current_conditions_data.json")
|
||||
)
|
||||
|
||||
# First entry in test data is a half day; we don't want that for this test
|
||||
del ec_data["daily_forecasts"][0]
|
||||
local_ec_data = copy.deepcopy(ec_data)
|
||||
del local_ec_data["daily_forecasts"][0]
|
||||
|
||||
await init_integration(hass, ec_data)
|
||||
await init_integration(hass, local_ec_data)
|
||||
|
||||
response = await hass.services.async_call(
|
||||
WEATHER_DOMAIN,
|
||||
@@ -44,15 +39,10 @@ async def test_forecast_daily(
|
||||
|
||||
|
||||
async def test_forecast_daily_with_some_previous_days_data(
|
||||
hass: HomeAssistant,
|
||||
snapshot: SnapshotAssertion,
|
||||
hass: HomeAssistant, snapshot: SnapshotAssertion, ec_data: dict[str, Any]
|
||||
) -> None:
|
||||
"""Test forecast with half day at start."""
|
||||
|
||||
ec_data = json.loads(
|
||||
load_fixture("environment_canada/current_conditions_data.json")
|
||||
)
|
||||
|
||||
await init_integration(hass, ec_data)
|
||||
|
||||
response = await hass.services.async_call(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user