Compare commits

...

72 Commits

Author SHA1 Message Date
Franck Nijhof 94516de724 2024.8.2 (#124069) 2024-08-16 18:43:41 +02:00
Joost Lekkerkerker a2027fc78c Exclude aiohappyeyeballs from license check (#124041) 2024-08-16 18:13:33 +02:00
Franck Nijhof be5577c2f9 Bump version to 2024.8.2 2024-08-16 18:02:52 +02:00
Joost Lekkerkerker 93dc08a05f Bump aiomealie to 0.8.1 (#124047) 2024-08-16 18:02:41 +02:00
Matthias Alphart def2ace4ec Fix loading KNX integration actions when not using YAML (#124027)
* Fix loading KNX integration services when not using YAML

* remove unnecessary comment

* Remove unreachable test
2024-08-16 18:02:38 +02:00
J. Nick Koston 4f0261d739 Bump bluetooth-adapters to 0.19.4 (#124018)
Fixes a call to enumerate USB devices that did blocking
I/O
2024-08-16 18:02:35 +02:00
Brett Adams 6103811de8 Fix rear trunk logic in Tessie (#124011)
Allow open to be anything not zero
2024-08-16 18:02:32 +02:00
Robert Svensson fd904c65a7 Bump aiounifi to v80 (#124004) 2024-08-16 18:02:29 +02:00
Joost Lekkerkerker 04bf8482b2 Re-enable concord232 (#124000) 2024-08-16 18:02:26 +02:00
Sid f5fd5e0457 Bump openwebifpy to 4.2.7 (#123995)
* Bump openwebifpy to 4.2.6

* Bump openwebifpy to 4.2.7

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
2024-08-16 18:02:23 +02:00
J. Nick Koston 0de89b42aa Ensure event entities are allowed for linked homekit config via YAML (#123994) 2024-08-16 18:02:20 +02:00
Erik Montnemery e8914552b1 Bump pyhomeworks to 1.1.1 (#123981) 2024-08-16 18:02:17 +02:00
Glenn Waters bfd302109e Environment Canada weather format fix (#123960)
* Add missing isoformat.

* Move fixture loading to common conftest.py

* Add deepcopy.
2024-08-16 18:02:14 +02:00
Andre Lengwenus 796ad47dd0 Bump pypck to 0.7.20 (#123948) 2024-08-16 18:02:11 +02:00
IceBotYT e9915463a9 Bump LaCrosse View to 1.0.2, fixes blocking call (#123935) 2024-08-16 18:02:07 +02:00
Michael 59aecda8cf Fix PI-Hole update entity when no update available (#123930)
show installed version when no update available
2024-08-16 17:58:24 +02:00
J. Nick Koston 7d00ccbbbc Bump pylutron_caseta to 0.21.1 (#123924) 2024-08-16 17:58:21 +02:00
Álvaro Fernández Rojas 55a911120c Handle timeouts on Airzone DHCP config flow (#123869)
airzone: config_flow: dhcp: catch timeout exception

Signed-off-by: Álvaro Fernández Rojas <noltari@gmail.com>
2024-08-16 17:58:18 +02:00
Michael 80abf90c87 Fix translation for integration not found repair issue (#123868)
* correct setp id in strings

* add issue_ignored string
2024-08-16 17:58:15 +02:00
Robert Resch 8539591307 Fix blocking I/O of SSLContext.load_default_certs in Ecovacs (#123856) 2024-08-16 17:58:12 +02:00
Michael 6234deeee1 Bump py-synologydsm-api to 2.4.5 (#123815)
bump py-synologydsm-api to 2.4.5
2024-08-16 17:57:59 +02:00
Louis Christ 81fabb1bfa Fix status update loop in bluesound integration (#123790)
* Fix retry loop for status update

* Use 'available' instead of _is_online

* Fix tests
2024-08-16 17:56:23 +02:00
Matthias Alphart ff4e5859cf Fix KNX UI Light color temperature DPT (#123778) 2024-08-16 17:13:31 +02:00
Matthias Alphart f2e42eafc7 Update xknx to 3.1.0 and fix climate read only mode (#123776) 2024-08-16 17:13:28 +02:00
Allen Porter 63f28ae2fe Bump python-nest-sdm to 4.0.6 (#123762) 2024-08-16 17:13:25 +02:00
Ian 5b6c6141c5 Bump py-nextbusnext to 2.0.4 (#123750) 2024-08-16 17:13:22 +02:00
Michael 396ef7a642 Fix error message in html5 (#123749) 2024-08-16 17:13:19 +02:00
Franck Nijhof 17f59a5665 Update wled to 0.20.2 (#123746) 2024-08-16 17:13:16 +02:00
David F. Mulcahey 10846dc97b Bump ZHA lib to 0.0.31 (#123743) 2024-08-16 17:13:13 +02:00
Álvaro Fernández Rojas 17bb00727d Update aioqsw to v0.4.1 (#123721) 2024-08-16 17:13:10 +02:00
Álvaro Fernández Rojas bc021dbbc6 Update aioairzone-cloud to v0.6.2 (#123719) 2024-08-16 17:13:06 +02:00
Álvaro Fernández Rojas e3cb9c0844 Update AEMET-OpenData to v0.5.4 (#123716) 2024-08-16 17:13:03 +02:00
David Knowles 050e2c9404 Bump pyschlage to 2024.8.0 (#123714) 2024-08-16 17:13:00 +02:00
Cyrill Raccaud 5ea447ba48 Fix startup block from Swiss public transport (#123704) 2024-08-16 17:12:57 +02:00
J. Nick Koston a23b063922 Bump aiohomekit to 3.2.2 (#123669) 2024-08-16 17:12:53 +02:00
Aidan Timson c269d57259 System Bridge package updates (#123657) 2024-08-16 17:12:50 +02:00
kingy444 d512f327c5 Bump pydaikin to 2.13.4 (#123623)
* bump pydaikin to 2.13.3

* bump pydaikin to 2.13.4
2024-08-16 17:12:46 +02:00
Maciej Bieniek 9bf8c5a54b Bump aioshelly to version 11.2.0 (#123602)
Bump aioshelly to version 11.2.0
2024-08-16 17:12:43 +02:00
J. Nick Koston 725e2f16f5 Ensure HomeKit connection is kept alive for devices that timeout too quickly (#123601) 2024-08-16 17:12:21 +02:00
G Johansson d98d0cdad0 Change WoL to be secondary on device info (#123591) 2024-08-16 17:07:24 +02:00
Noah Husby e2f4aa893f Fix secondary russound controller discovery failure (#123590) 2024-08-16 17:07:21 +02:00
Matthias Alphart 6b81fa89d3 Update knx-frontend to 2024.8.9.225351 (#123557) 2024-08-16 17:07:18 +02:00
J. Nick Koston c886587915 Bump aiohttp to 3.10.3 (#123549) 2024-08-16 17:07:15 +02:00
Phill (pssc) 059d3eed98 Handle Yamaha ValueError (#123547)
* fix yamaha remove info logging

* ruff

* fix yamnaha supress rxv.find UnicodeDecodeError

* fix formatting

* make more realistic

* make more realistic and use parms

* add value error after more feedback

* ruff format

* Update homeassistant/components/yamaha/media_player.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* remove unused method

* add more debugging

* Increase discovery timeout add more debug allow config to overrite dicovery for name

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
2024-08-16 17:07:12 +02:00
Erik Montnemery f9ae2b4453 Drop violating rows before adding foreign constraints in DB schema 44 migration (#123454)
* Drop violating rows before adding foreign constraints

* Don't delete rows with null-references

* Only delete rows when integrityerror is caught

* Move restore of dropped foreign key constraints to a separate migration step

* Use aliases for tables

* Update homeassistant/components/recorder/migration.py

* Update test

* Don't use alias for table we're deleting from, improve test

* Fix MySQL

* Update instead of deleting in case of self references

* Improve log messages

* Batch updates

* Add workaround for unsupported LIMIT in PostgreSQL

* Simplify

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
2024-08-16 17:07:09 +02:00
ilan 742c7ba23f Fix Madvr sensor values on startup (#122479)
* fix: add startup values

* fix: update snap

* fix: use native value to show None
2024-08-16 17:07:06 +02:00
wittypluck e7ae5c5c24 Avoid Exception on Glances missing key (#114628)
* Handle case of sensors removed server side

* Update available state on value update

* Set uptime to None if key is missing

* Replace _attr_available by _data_valid
2024-08-16 17:07:02 +02:00
Franck Nijhof ae4fc9504a 2024.8.1 (#123544) 2024-08-10 19:32:02 +02:00
Franck Nijhof 2ef337ec2e Bump version to 2024.8.1 2024-08-10 18:41:57 +02:00
cnico 723b7bd532 Upgrade chacon_dio_api to version 1.2.0 (#123528)
Upgrade api version 1.2.0 with the first user feedback improvement
2024-08-10 18:41:39 +02:00
Joost Lekkerkerker 4fdb11b0d8 Bump AirGradient to 0.8.0 (#123527) 2024-08-10 18:41:36 +02:00
Matt Way fe2e6c37f4 Bump pydaikin to 2.13.2 (#123519) 2024-08-10 18:41:32 +02:00
Michael 4a75c55a8f Fix cleanup of old orphan device entries in AVM Fritz!Tools (#123516)
fix cleanup of old orphan device entries
2024-08-10 18:41:29 +02:00
Duco Sebel dfb59469cf Bumb python-homewizard-energy to 6.2.0 (#123514) 2024-08-10 18:41:26 +02:00
David F. Mulcahey bdb2e1e2e9 Bump zha lib to 0.0.30 (#123499) 2024-08-10 18:41:22 +02:00
Franck Nijhof c4f6f1e3d8 Update frontend to 20240809.0 (#123485) 2024-08-10 18:41:19 +02:00
Louis Christ fb3eae54ea Fix startup blocked by bluesound integration (#123483) 2024-08-10 18:41:16 +02:00
Jake Martin d3f8fce788 Bump monzopy to 1.3.2 (#123480) 2024-08-10 18:41:13 +02:00
Steve Easley 44e58a8c87 Bump pyjvcprojector to 1.0.12 to fix blocking call (#123473) 2024-08-10 18:41:09 +02:00
puddly 3d3879b0db Bump ZHA library to 0.0.29 (#123464)
* Bump zha to 0.0.29

* Pass the Core timezone to ZHA

* Add a unit test
2024-08-10 18:41:06 +02:00
Franck Nijhof a8b1eb34f3 Support action YAML syntax in old-style notify groups (#123457) 2024-08-10 18:41:03 +02:00
Matrix fd77058def Bump YoLink API to 0.4.7 (#123441) 2024-08-10 18:41:00 +02:00
Brett Adams b147ca6c5b Add missing logger to Tessie (#123413) 2024-08-10 18:40:57 +02:00
dupondje 670c4cacfa Also migrate dsmr entries for devices with correct serial (#123407)
dsmr: also migrate entries for devices with correct serial

When the dsmr code could not find the serial_nr for the gas meter,
it creates the gas meter device with the entry_id as identifier.

But when there is a correct serial_nr, it will use that as identifier
for the dsmr gas device.

Now the migration code did not take this into account, so migration to
the new name failed since it didn't look for the device with correct
serial_nr.

This commit fixes this and adds a test for this.
2024-08-10 18:40:53 +02:00
J. Nick Koston 1ed0a89303 Bump aiohttp to 3.10.2 (#123394) 2024-08-10 18:40:50 +02:00
J. Nick Koston ab0597da7b Ensure legacy event foreign key is removed from the states table when a previous rebuild failed (#123388)
* Ensure legacy event foreign key is removed from the states table

If the system ran out of disk space removing the FK, it would
fail. #121938 fixed that to try again, however that PR was made
ineffective by #122069 since it will never reach the check.

To solve this, the migration version is incremented to 2, and
the migration is no longer marked as done unless the rebuild
/fk removal is successful.

* fix logic for mysql

* fix test

* asserts

* coverage

* coverage

* narrow test

* fixes

* split tests

* should have skipped

* fixture must be used
2024-08-10 18:40:47 +02:00
Erik Montnemery a3db6bc8fa Revert "Fix blocking I/O while validating config schema" (#123377) 2024-08-10 18:40:44 +02:00
Noah Husby 9bfc8f6e27 Bump aiorussound to 2.2.2 (#123319) 2024-08-10 18:40:41 +02:00
J. Nick Koston 6fddef2dc5 Fix doorbird with externally added events (#123313) 2024-08-10 18:40:38 +02:00
fustom ec08a85aa0 Fix limit and order property for transmission integration (#123305) 2024-08-10 18:40:35 +02:00
Evgeny de7af575c5 Bump OpenWeatherMap to 0.1.1 (#120178)
* add owm modes

* fix tests

* fix modes

* remove sensors

* Update homeassistant/components/openweathermap/sensor.py

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2024-08-10 18:40:32 +02:00
Tom Brien d3831bae4e Add support for v3 Coinbase API (#116345)
* Add support for v3 Coinbase API

* Add deps

* Move tests
2024-08-10 18:40:28 +02:00
116 changed files with 2094 additions and 551 deletions
+1 -1
View File
@@ -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"]
}
+84 -19
View File
@@ -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
+10 -1
View File
@@ -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"]
}
+41 -28
View File
@@ -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."]
}
+1 -1
View File
@@ -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):
+34 -33
View File
@@ -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)
+22 -14
View File
@@ -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 {}
+14 -11
View File
@@ -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()
)
+30 -3
View File
@@ -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": {
+5 -2
View File
@@ -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"]
}
+1 -1
View File
@@ -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"]
}
+2 -10
View File
@@ -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
+22 -7
View File
@@ -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)
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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"]
}
+1 -1
View File
@@ -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.",
+12 -1
View File
@@ -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"]
}
+1 -1
View File
@@ -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"]
}
+1 -1
View File
@@ -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:
+7 -1
View File
@@ -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__)
+224 -37
View File
@@ -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."]
}
+2 -2
View File
@@ -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."""
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -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
View File
@@ -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"]
}
+17 -2
View File
@@ -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)
+1 -1
View File
@@ -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:
+2
View File
@@ -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),
)
+1 -1
View File
@@ -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
View File
@@ -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,
+1 -1
View File
@@ -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)
+3 -3
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+32 -3
View File
@@ -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,
+67 -1
View File
@@ -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
+28
View File
@@ -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({
+84 -6
View File
@@ -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/"
}
}
}
+1 -1
View File
@@ -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