mirror of
https://github.com/home-assistant/core.git
synced 2026-05-05 20:34:52 +02:00
Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bb9be2382 | |||
| 6581bad7ce | |||
| 197634503f | |||
| 32fc0e03a5 | |||
| 2e9ea0c934 | |||
| 856f68252b | |||
| 2789747b0f | |||
| 45d14739c5 | |||
| d0f95d84b4 | |||
| 1e852e761c | |||
| c3859f9170 | |||
| 6df4fc6708 | |||
| b297b78086 | |||
| 4bdf87d383 | |||
| e47364f34d | |||
| fe7d32dc5d | |||
| 62a003a053 | |||
| b5d1421dfd | |||
| ebab2bd0f9 | |||
| e7babb4266 | |||
| 1a042c2dad | |||
| 731ca046f6 | |||
| c844276e95 | |||
| 9f9cdb62eb | |||
| c73830439f | |||
| 940b5d62b4 | |||
| b3454bfd9c | |||
| 834847988d | |||
| caf15534bb | |||
| 10cb2e31c4 | |||
| 85c9f9facf | |||
| 5ff7b3bb1a | |||
| e5ba423d6d | |||
| b30d4ef7cf | |||
| 00e563f1b8 | |||
| cf06f3b81d | |||
| a781fcca86 | |||
| 764550f2e1 | |||
| 7396bcc585 | |||
| 7e6b087773 | |||
| 71ce7373a3 | |||
| 33bb9c230b | |||
| f0f2c12d91 | |||
| 2840821594 | |||
| edfd83c3a7 | |||
| ee88f34a91 | |||
| fa4c250001 | |||
| 59d6f827c3 | |||
| 26ea02aa8f | |||
| d73b86132b | |||
| 8034faadca | |||
| 3c2b7c0d69 | |||
| 563ad02c65 | |||
| fe89b663e7 | |||
| dcd07d3135 | |||
| 8bf2299407 | |||
| 9c689d757c | |||
| 4e4fc1767f | |||
| cc3c5772c5 | |||
| 6ba6991ecd | |||
| d52d068469 | |||
| 09b3611a63 | |||
| ab2f05d3e9 | |||
| 90ac0c870f | |||
| 0fd113db59 | |||
| 1b43323f5e | |||
| 6108e581b1 | |||
| c8c68f05ec | |||
| b80467dc58 | |||
| 6e9f0eca03 | |||
| cc6a2f0338 | |||
| 6ebf2ec9ec | |||
| 3ba59fbebe | |||
| f3fab5c1f5 |
+31
-8
@@ -11,22 +11,45 @@ WORKDIR /usr/src
|
||||
COPY requirements.txt homeassistant/
|
||||
COPY homeassistant/package_constraints.txt homeassistant/homeassistant/
|
||||
RUN \
|
||||
pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \
|
||||
-r homeassistant/requirements.txt --use-deprecated=legacy-resolver
|
||||
pip3 install \
|
||||
--no-cache-dir \
|
||||
--no-index \
|
||||
--only-binary=:all: \
|
||||
--find-links "${WHEELS_LINKS}" \
|
||||
--use-deprecated=legacy-resolver \
|
||||
-r homeassistant/requirements.txt
|
||||
|
||||
COPY requirements_all.txt home_assistant_frontend-* homeassistant/
|
||||
RUN \
|
||||
if ls homeassistant/home_assistant_frontend*.whl 1> /dev/null 2>&1; then \
|
||||
pip3 install --no-cache-dir --no-index homeassistant/home_assistant_frontend-*.whl; \
|
||||
pip3 install \
|
||||
--no-cache-dir \
|
||||
--no-index \
|
||||
homeassistant/home_assistant_frontend-*.whl; \
|
||||
fi \
|
||||
&& pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \
|
||||
-r homeassistant/requirements_all.txt --use-deprecated=legacy-resolver
|
||||
&& \
|
||||
LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" \
|
||||
MALLOC_CONF="background_thread:true,metadata_thp:auto,dirty_decay_ms:20000,muzzy_decay_ms:20000" \
|
||||
pip3 install \
|
||||
--no-cache-dir \
|
||||
--no-index \
|
||||
--only-binary=:all: \
|
||||
--find-links "${WHEELS_LINKS}" \
|
||||
--use-deprecated=legacy-resolver \
|
||||
-r homeassistant/requirements_all.txt
|
||||
|
||||
## Setup Home Assistant Core
|
||||
COPY . homeassistant/
|
||||
RUN \
|
||||
pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \
|
||||
-e ./homeassistant --use-deprecated=legacy-resolver \
|
||||
&& python3 -m compileall homeassistant/homeassistant
|
||||
pip3 install \
|
||||
--no-cache-dir \
|
||||
--no-index \
|
||||
--only-binary=:all: \
|
||||
--find-links "${WHEELS_LINKS}" \
|
||||
--use-deprecated=legacy-resolver \
|
||||
-e ./homeassistant \
|
||||
&& python3 -m compileall \
|
||||
homeassistant/homeassistant
|
||||
|
||||
# Home Assistant S6-Overlay
|
||||
COPY rootfs /
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "AdGuard Home",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/adguard",
|
||||
"requirements": ["adguardhome==0.5.1"],
|
||||
"requirements": ["adguardhome==0.6.1"],
|
||||
"codeowners": ["@frenck"],
|
||||
"iot_class": "local_polling",
|
||||
"integration_type": "service",
|
||||
|
||||
@@ -69,6 +69,7 @@ class BMWDeviceTracker(BMWBaseEntity, TrackerEntity):
|
||||
return (
|
||||
self.vehicle.vehicle_location.location[0]
|
||||
if self.vehicle.is_vehicle_tracking_enabled
|
||||
and self.vehicle.vehicle_location.location
|
||||
else None
|
||||
)
|
||||
|
||||
@@ -78,6 +79,7 @@ class BMWDeviceTracker(BMWBaseEntity, TrackerEntity):
|
||||
return (
|
||||
self.vehicle.vehicle_location.location[1]
|
||||
if self.vehicle.is_vehicle_tracking_enabled
|
||||
and self.vehicle.vehicle_location.location
|
||||
else None
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "bmw_connected_drive",
|
||||
"name": "BMW Connected Drive",
|
||||
"documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive",
|
||||
"requirements": ["bimmer_connected==0.10.4"],
|
||||
"requirements": ["bimmer_connected==0.12.0"],
|
||||
"codeowners": ["@gerard33", "@rikroe"],
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"service_data_uuid": "0000fcd2-0000-1000-8000-00805f9b34fb"
|
||||
}
|
||||
],
|
||||
"requirements": ["bthome-ble==2.4.0"],
|
||||
"requirements": ["bthome-ble==2.4.1"],
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": ["@Ernst79"],
|
||||
"iot_class": "local_push"
|
||||
|
||||
@@ -10,6 +10,7 @@ from pycfdns.exceptions import (
|
||||
CloudflareAuthenticationException,
|
||||
CloudflareConnectionException,
|
||||
CloudflareException,
|
||||
CloudflareZoneException,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -47,7 +48,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
zone_id = await cfupdate.get_zone_id()
|
||||
except CloudflareAuthenticationException as error:
|
||||
raise ConfigEntryAuthFailed from error
|
||||
except CloudflareConnectionException as error:
|
||||
except (CloudflareConnectionException, CloudflareZoneException) as error:
|
||||
raise ConfigEntryNotReady from error
|
||||
|
||||
async def update_records(now):
|
||||
|
||||
@@ -21,7 +21,6 @@ from .devolo_device import DevoloDeviceEntity
|
||||
DEVICE_CLASS_MAPPING = {
|
||||
"battery": SensorDeviceClass.BATTERY,
|
||||
"temperature": SensorDeviceClass.TEMPERATURE,
|
||||
"light": SensorDeviceClass.ILLUMINANCE,
|
||||
"humidity": SensorDeviceClass.HUMIDITY,
|
||||
"current": SensorDeviceClass.POWER,
|
||||
"total": SensorDeviceClass.ENERGY,
|
||||
|
||||
@@ -28,6 +28,7 @@ from homeassistant.const import (
|
||||
CONF_HOST,
|
||||
CONF_PORT,
|
||||
EVENT_HOMEASSISTANT_STOP,
|
||||
UnitOfEnergy,
|
||||
UnitOfVolume,
|
||||
)
|
||||
from homeassistant.core import CoreState, Event, HomeAssistant, callback
|
||||
@@ -591,6 +592,21 @@ class DSMREntity(SensorEntity):
|
||||
"""Entity is only available if there is a telegram."""
|
||||
return self.telegram is not None
|
||||
|
||||
@property
|
||||
def device_class(self) -> SensorDeviceClass | None:
|
||||
"""Return the device class of this entity."""
|
||||
device_class = super().device_class
|
||||
|
||||
# Override device class for gas sensors providing energy units, like
|
||||
# kWh, MWh, GJ, etc. In those cases, the class should be energy, not gas
|
||||
with suppress(ValueError):
|
||||
if device_class == SensorDeviceClass.GAS and UnitOfEnergy(
|
||||
str(self.native_unit_of_measurement)
|
||||
):
|
||||
return SensorDeviceClass.ENERGY
|
||||
|
||||
return device_class
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of sensor, if available, translate if needed."""
|
||||
|
||||
@@ -560,8 +560,8 @@ SENSORS: tuple[DSMRReaderSensorEntityDescription, ...] = (
|
||||
DSMRReaderSensorEntityDescription(
|
||||
key="dsmr/consumption/quarter-hour-peak-electricity/average_delivered",
|
||||
name="Previous quarter-hour peak usage",
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
native_unit_of_measurement=UnitOfPower.KILO_WATT,
|
||||
),
|
||||
DSMRReaderSensorEntityDescription(
|
||||
key="dsmr/consumption/quarter-hour-peak-electricity/read_at_start",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Rheem EcoNet Products",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/econet",
|
||||
"requirements": ["pyeconet==0.1.17"],
|
||||
"requirements": ["pyeconet==0.1.18"],
|
||||
"codeowners": ["@vangorra", "@w1ll1am23"],
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["paho_mqtt", "pyeconet"]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "emulated_kasa",
|
||||
"name": "Emulated Kasa",
|
||||
"documentation": "https://www.home-assistant.io/integrations/emulated_kasa",
|
||||
"requirements": ["sense_energy==0.11.0"],
|
||||
"requirements": ["sense_energy==0.11.1"],
|
||||
"codeowners": ["@kbickar"],
|
||||
"quality_scale": "internal",
|
||||
"iot_class": "local_push",
|
||||
|
||||
@@ -41,20 +41,20 @@ SUPPORTED_STATE_CLASSES = {
|
||||
SensorStateClass.TOTAL_INCREASING,
|
||||
}
|
||||
VALID_ENERGY_UNITS: set[str] = {
|
||||
UnitOfEnergy.WATT_HOUR,
|
||||
UnitOfEnergy.GIGA_JOULE,
|
||||
UnitOfEnergy.KILO_WATT_HOUR,
|
||||
UnitOfEnergy.MEGA_WATT_HOUR,
|
||||
UnitOfEnergy.GIGA_JOULE,
|
||||
UnitOfEnergy.WATT_HOUR,
|
||||
}
|
||||
VALID_ENERGY_UNITS_GAS = {
|
||||
UnitOfVolume.CUBIC_FEET,
|
||||
UnitOfVolume.CENTUM_CUBIC_FEET,
|
||||
UnitOfVolume.CUBIC_FEET,
|
||||
UnitOfVolume.CUBIC_METERS,
|
||||
*VALID_ENERGY_UNITS,
|
||||
}
|
||||
VALID_VOLUME_UNITS_WATER: set[str] = {
|
||||
UnitOfVolume.CUBIC_FEET,
|
||||
UnitOfVolume.CENTUM_CUBIC_FEET,
|
||||
UnitOfVolume.CUBIC_FEET,
|
||||
UnitOfVolume.CUBIC_METERS,
|
||||
UnitOfVolume.GALLONS,
|
||||
UnitOfVolume.LITERS,
|
||||
|
||||
@@ -22,10 +22,10 @@ from .const import DOMAIN
|
||||
ENERGY_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.ENERGY,)
|
||||
ENERGY_USAGE_UNITS = {
|
||||
sensor.SensorDeviceClass.ENERGY: (
|
||||
UnitOfEnergy.GIGA_JOULE,
|
||||
UnitOfEnergy.KILO_WATT_HOUR,
|
||||
UnitOfEnergy.MEGA_WATT_HOUR,
|
||||
UnitOfEnergy.WATT_HOUR,
|
||||
UnitOfEnergy.GIGA_JOULE,
|
||||
)
|
||||
}
|
||||
ENERGY_PRICE_UNITS = tuple(
|
||||
@@ -39,12 +39,16 @@ GAS_USAGE_DEVICE_CLASSES = (
|
||||
)
|
||||
GAS_USAGE_UNITS = {
|
||||
sensor.SensorDeviceClass.ENERGY: (
|
||||
UnitOfEnergy.WATT_HOUR,
|
||||
UnitOfEnergy.GIGA_JOULE,
|
||||
UnitOfEnergy.KILO_WATT_HOUR,
|
||||
UnitOfEnergy.MEGA_WATT_HOUR,
|
||||
UnitOfEnergy.GIGA_JOULE,
|
||||
UnitOfEnergy.WATT_HOUR,
|
||||
),
|
||||
sensor.SensorDeviceClass.GAS: (
|
||||
UnitOfVolume.CENTUM_CUBIC_FEET,
|
||||
UnitOfVolume.CUBIC_FEET,
|
||||
UnitOfVolume.CUBIC_METERS,
|
||||
),
|
||||
sensor.SensorDeviceClass.GAS: (UnitOfVolume.CUBIC_METERS, UnitOfVolume.CUBIC_FEET),
|
||||
}
|
||||
GAS_PRICE_UNITS = tuple(
|
||||
f"/{unit}" for units in GAS_USAGE_UNITS.values() for unit in units
|
||||
@@ -54,8 +58,9 @@ GAS_PRICE_UNIT_ERROR = "entity_unexpected_unit_gas_price"
|
||||
WATER_USAGE_DEVICE_CLASSES = (sensor.SensorDeviceClass.WATER,)
|
||||
WATER_USAGE_UNITS = {
|
||||
sensor.SensorDeviceClass.WATER: (
|
||||
UnitOfVolume.CUBIC_METERS,
|
||||
UnitOfVolume.CENTUM_CUBIC_FEET,
|
||||
UnitOfVolume.CUBIC_FEET,
|
||||
UnitOfVolume.CUBIC_METERS,
|
||||
UnitOfVolume.GALLONS,
|
||||
UnitOfVolume.LITERS,
|
||||
),
|
||||
|
||||
@@ -228,7 +228,6 @@ AQHI_SENSOR = ECSensorEntityDescription(
|
||||
key="aqhi",
|
||||
name="AQHI",
|
||||
device_class=SensorDeviceClass.AQI,
|
||||
native_unit_of_measurement="AQI",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=_get_aqhi_value,
|
||||
)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "frontend",
|
||||
"name": "Home Assistant Frontend",
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"requirements": ["home-assistant-frontend==20230102.0"],
|
||||
"requirements": ["home-assistant-frontend==20230110.0"],
|
||||
"dependencies": [
|
||||
"api",
|
||||
"auth",
|
||||
|
||||
@@ -47,10 +47,19 @@ async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
for target in call.data[ATTR_DEVICE_ID]:
|
||||
device = registry.async_get(target)
|
||||
if device:
|
||||
coordinator = hass.data[DOMAIN][list(device.config_entries)[0]]
|
||||
# fully_method(coordinator.fully, *args, **kwargs) would make
|
||||
# test_services.py fail.
|
||||
await getattr(coordinator.fully, fully_method.__name__)(*args, **kwargs)
|
||||
for key in device.config_entries:
|
||||
entry = hass.config_entries.async_get_entry(key)
|
||||
if not entry:
|
||||
continue
|
||||
if entry.domain != DOMAIN:
|
||||
continue
|
||||
coordinator = hass.data[DOMAIN][key]
|
||||
# fully_method(coordinator.fully, *args, **kwargs) would make
|
||||
# test_services.py fail.
|
||||
await getattr(coordinator.fully, fully_method.__name__)(
|
||||
*args, **kwargs
|
||||
)
|
||||
break
|
||||
|
||||
async def async_load_url(call: ServiceCall) -> None:
|
||||
"""Load a URL on the Fully Kiosk Browser."""
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"dependencies": ["application_credentials"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/calendar.google/",
|
||||
"requirements": ["gcal-sync==4.1.0", "oauth2client==4.1.3"],
|
||||
"requirements": ["gcal-sync==4.1.2", "oauth2client==4.1.3"],
|
||||
"codeowners": ["@allenporter"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["googleapiclient"]
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
"""application_credentials platform for Google Sheets."""
|
||||
import oauth2client
|
||||
|
||||
from homeassistant.components.application_credentials import AuthorizationServer
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
@@ -8,17 +6,15 @@ from homeassistant.core import HomeAssistant
|
||||
async def async_get_authorization_server(hass: HomeAssistant) -> AuthorizationServer:
|
||||
"""Return authorization server."""
|
||||
return AuthorizationServer(
|
||||
oauth2client.GOOGLE_AUTH_URI,
|
||||
oauth2client.GOOGLE_TOKEN_URI,
|
||||
"https://accounts.google.com/o/oauth2/v2/auth",
|
||||
"https://oauth2.googleapis.com/token",
|
||||
)
|
||||
|
||||
|
||||
async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]:
|
||||
"""Return description placeholders for the credentials dialog."""
|
||||
return {
|
||||
"oauth_consent_url": (
|
||||
"https://console.cloud.google.com/apis/credentials/consent"
|
||||
),
|
||||
"oauth_consent_url": "https://console.cloud.google.com/apis/credentials/consent",
|
||||
"more_info_url": "https://www.home-assistant.io/integrations/google_sheets/",
|
||||
"oauth_creds_url": "https://console.cloud.google.com/apis/credentials",
|
||||
}
|
||||
|
||||
@@ -250,6 +250,24 @@ class ConfigFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
await self.async_set_unique_id(unique_id)
|
||||
self._abort_if_unique_id_configured(updates={CONF_URL: url})
|
||||
|
||||
def _is_supported_device() -> bool:
|
||||
"""
|
||||
See if we are looking at a possibly supported device.
|
||||
|
||||
Matching solely on SSDP data does not yield reliable enough results.
|
||||
"""
|
||||
try:
|
||||
with Connection(url=url, timeout=CONNECTION_TIMEOUT) as conn:
|
||||
basic_info = Client(conn).device.basic_information()
|
||||
except ResponseErrorException: # API compatible error
|
||||
return True
|
||||
except Exception: # API incompatible error # pylint: disable=broad-except
|
||||
return False
|
||||
return isinstance(basic_info, dict) # Crude content check
|
||||
|
||||
if not await self.hass.async_add_executor_job(_is_supported_device):
|
||||
return self.async_abort(reason="unsupported_device")
|
||||
|
||||
self.context.update(
|
||||
{
|
||||
"title_placeholders": {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/huawei_lte",
|
||||
"requirements": [
|
||||
"huawei-lte-api==1.6.7",
|
||||
"huawei-lte-api==1.6.11",
|
||||
"stringcase==1.2.0",
|
||||
"url-normalize==1.4.3"
|
||||
],
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||
"unsupported_device": "Unsupported device"
|
||||
},
|
||||
"error": {
|
||||
"connection_timeout": "Connection timeout",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"not_huawei_lte": "Not a Huawei LTE device",
|
||||
"reauth_successful": "Re-authentication was successful"
|
||||
"reauth_successful": "Re-authentication was successful",
|
||||
"unsupported_device": "Unsupported device"
|
||||
},
|
||||
"error": {
|
||||
"connection_timeout": "Connection timeout",
|
||||
|
||||
@@ -28,7 +28,7 @@ DATA_HYDRAWISE = "hydrawise"
|
||||
DOMAIN = "hydrawise"
|
||||
DEFAULT_WATERING_TIME = 15
|
||||
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
SCAN_INTERVAL = timedelta(seconds=120)
|
||||
|
||||
SIGNAL_UPDATE_HYDRAWISE = "hydrawise_update"
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""DataUpdateCoordinator for LaCrosse View."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import timedelta
|
||||
from time import time
|
||||
|
||||
from lacrosse_view import HTTPError, LaCrosse, Location, LoginError, Sensor
|
||||
|
||||
@@ -30,7 +31,7 @@ class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]):
|
||||
) -> None:
|
||||
"""Initialize DataUpdateCoordinator for LaCrosse View."""
|
||||
self.api = api
|
||||
self.last_update = datetime.utcnow()
|
||||
self.last_update = time()
|
||||
self.username = entry.data["username"]
|
||||
self.password = entry.data["password"]
|
||||
self.hass = hass
|
||||
@@ -45,26 +46,22 @@ class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]):
|
||||
|
||||
async def _async_update_data(self) -> list[Sensor]:
|
||||
"""Get the data for LaCrosse View."""
|
||||
now = datetime.utcnow()
|
||||
now = int(time())
|
||||
|
||||
if self.last_update < now - timedelta(minutes=59): # Get new token
|
||||
if self.last_update < now - 59 * 60: # Get new token once in a hour
|
||||
self.last_update = now
|
||||
try:
|
||||
await self.api.login(self.username, self.password)
|
||||
except LoginError as error:
|
||||
raise ConfigEntryAuthFailed from error
|
||||
|
||||
# Get the timestamp for yesterday at 6 PM (this is what is used in the app, i noticed it when proxying the request)
|
||||
yesterday = now - timedelta(days=1)
|
||||
yesterday = yesterday.replace(hour=18, minute=0, second=0, microsecond=0)
|
||||
yesterday_timestamp = datetime.timestamp(yesterday)
|
||||
|
||||
try:
|
||||
# Fetch last hour of data
|
||||
sensors = await self.api.get_sensors(
|
||||
location=Location(id=self.id, name=self.name),
|
||||
tz=self.hass.config.time_zone,
|
||||
start=str(int(yesterday_timestamp)),
|
||||
end=str(int(datetime.timestamp(now))),
|
||||
start=str(now - 3600),
|
||||
end=str(now),
|
||||
)
|
||||
except HTTPError as error:
|
||||
raise ConfigEntryNotReady from error
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "LCN",
|
||||
"config_flow": false,
|
||||
"documentation": "https://www.home-assistant.io/integrations/lcn",
|
||||
"requirements": ["pypck==0.7.15"],
|
||||
"requirements": ["pypck==0.7.16"],
|
||||
"codeowners": ["@alengwenus"],
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pypck"]
|
||||
|
||||
@@ -194,7 +194,7 @@ class VarAbs(LcnServiceCall):
|
||||
vol.Required(CONF_VARIABLE): vol.All(
|
||||
vol.Upper, vol.In(VARIABLES + SETPOINTS)
|
||||
),
|
||||
vol.Optional(CONF_VALUE, default=0): cv.positive_int,
|
||||
vol.Optional(CONF_VALUE, default=0): vol.Coerce(float),
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT, default="native"): vol.All(
|
||||
vol.Upper, vol.In(VAR_UNITS)
|
||||
),
|
||||
@@ -234,7 +234,7 @@ class VarRel(LcnServiceCall):
|
||||
vol.Required(CONF_VARIABLE): vol.All(
|
||||
vol.Upper, vol.In(VARIABLES + SETPOINTS + THRESHOLDS)
|
||||
),
|
||||
vol.Optional(CONF_VALUE, default=0): int,
|
||||
vol.Optional(CONF_VALUE, default=0): vol.Coerce(float),
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT, default="native"): vol.All(
|
||||
vol.Upper, vol.In(VAR_UNITS)
|
||||
),
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/life360",
|
||||
"codeowners": ["@pnbruckner"],
|
||||
"requirements": ["life360==5.3.0"],
|
||||
"requirements": ["life360==5.5.0"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["life360"]
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Litter-Robot",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/litterrobot",
|
||||
"requirements": ["pylitterbot==2022.12.0"],
|
||||
"requirements": ["pylitterbot==2023.1.1"],
|
||||
"codeowners": ["@natekspencer", "@tkdrob"],
|
||||
"dhcp": [{ "hostname": "litter-robot4" }],
|
||||
"iot_class": "cloud_push",
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"""Support for Litter-Robot updates."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
from pylitterbot import LitterRobot4
|
||||
@@ -17,12 +16,12 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
from homeassistant.helpers.start import async_at_start
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import LitterRobotEntity, LitterRobotHub
|
||||
|
||||
SCAN_INTERVAL = timedelta(days=1)
|
||||
|
||||
FIRMWARE_UPDATE_ENTITY = UpdateEntityDescription(
|
||||
key="firmware",
|
||||
name="Firmware",
|
||||
@@ -43,7 +42,7 @@ async def async_setup_entry(
|
||||
for robot in robots
|
||||
if isinstance(robot, LitterRobot4)
|
||||
]
|
||||
async_add_entities(entities)
|
||||
async_add_entities(entities, True)
|
||||
|
||||
|
||||
class RobotUpdateEntity(LitterRobotEntity[LitterRobot4], UpdateEntity):
|
||||
@@ -53,16 +52,6 @@ class RobotUpdateEntity(LitterRobotEntity[LitterRobot4], UpdateEntity):
|
||||
UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
robot: LitterRobot4,
|
||||
hub: LitterRobotHub,
|
||||
description: UpdateEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize a Litter-Robot update entity."""
|
||||
super().__init__(robot, hub, description)
|
||||
self._poll_unsub: Callable[[], None] | None = None
|
||||
|
||||
@property
|
||||
def installed_version(self) -> str:
|
||||
"""Version installed and in use."""
|
||||
@@ -73,39 +62,27 @@ class RobotUpdateEntity(LitterRobotEntity[LitterRobot4], UpdateEntity):
|
||||
"""Update installation progress."""
|
||||
return self.robot.firmware_update_triggered
|
||||
|
||||
async def _async_update(self, _: HomeAssistant | datetime | None = None) -> None:
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Set polling to True."""
|
||||
return True
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Update the entity."""
|
||||
self._poll_unsub = None
|
||||
|
||||
if await self.robot.has_firmware_update():
|
||||
latest_version = await self.robot.get_latest_firmware()
|
||||
else:
|
||||
latest_version = self.installed_version
|
||||
|
||||
if self._attr_latest_version != self.installed_version:
|
||||
# If the robot has a firmware update already in progress, checking for the
|
||||
# latest firmware informs that an update has already been triggered, no
|
||||
# firmware information is returned and we won't know the latest version.
|
||||
if not self.robot.firmware_update_triggered:
|
||||
latest_version = await self.robot.get_latest_firmware(True)
|
||||
if not await self.robot.has_firmware_update():
|
||||
latest_version = self.robot.firmware
|
||||
self._attr_latest_version = latest_version
|
||||
self.async_write_ha_state()
|
||||
|
||||
self._poll_unsub = async_call_later(
|
||||
self.hass, timedelta(days=1), self._async_update
|
||||
)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Set up a listener for the entity."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_on_remove(async_at_start(self.hass, self._async_update))
|
||||
|
||||
async def async_install(
|
||||
self, version: str | None, backup: bool, **kwargs: Any
|
||||
) -> None:
|
||||
"""Install an update."""
|
||||
if await self.robot.has_firmware_update():
|
||||
if await self.robot.has_firmware_update(True):
|
||||
if not await self.robot.update_firmware():
|
||||
message = f"Unable to start firmware update on {self.robot.name}"
|
||||
raise HomeAssistantError(message)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Call when entity will be removed."""
|
||||
if self._poll_unsub:
|
||||
self._poll_unsub()
|
||||
self._poll_unsub = None
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Local Calendar",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
|
||||
"requirements": ["ical==4.2.8"],
|
||||
"requirements": ["ical==4.2.9"],
|
||||
"codeowners": ["@allenporter"],
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["ical"]
|
||||
|
||||
@@ -59,7 +59,7 @@ CONF_EXPIRE_AFTER = "expire_after"
|
||||
|
||||
PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
|
||||
vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int,
|
||||
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
|
||||
@@ -6,7 +6,7 @@ import functools
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import button
|
||||
from homeassistant.components.button import ButtonEntity
|
||||
from homeassistant.components.button import DEVICE_CLASSES_SCHEMA, ButtonEntity
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -39,7 +39,7 @@ PLATFORM_SCHEMA_MODERN = MQTT_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Required(CONF_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_DEVICE_CLASS): button.DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_PAYLOAD_PRESS, default=DEFAULT_PAYLOAD_PRESS): cv.string,
|
||||
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
|
||||
|
||||
@@ -161,7 +161,7 @@ def validate_options(config: ConfigType) -> ConfigType:
|
||||
_PLATFORM_SCHEMA_BASE = MQTT_BASE_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
|
||||
vol.Optional(CONF_GET_POSITION_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
|
||||
|
||||
@@ -87,7 +87,7 @@ def validate_config(config: ConfigType) -> ConfigType:
|
||||
_PLATFORM_SCHEMA_BASE = MQTT_RW_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_COMMAND_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
|
||||
vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): vol.Coerce(float),
|
||||
vol.Optional(CONF_MIN, default=DEFAULT_MIN_VALUE): vol.Coerce(float),
|
||||
vol.Optional(CONF_MODE, default=NumberMode.AUTO): vol.Coerce(NumberMode),
|
||||
|
||||
@@ -115,6 +115,7 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity):
|
||||
discovery_data: DiscoveryInfoType | None,
|
||||
) -> None:
|
||||
"""Initialize the MQTT select."""
|
||||
self._attr_current_option = None
|
||||
SelectEntity.__init__(self)
|
||||
MqttEntity.__init__(self, hass, config, config_entry, discovery_data)
|
||||
|
||||
@@ -125,7 +126,6 @@ class MqttSelect(MqttEntity, SelectEntity, RestoreEntity):
|
||||
|
||||
def _setup_from_config(self, config: ConfigType) -> None:
|
||||
"""(Re)Setup the entity."""
|
||||
self._attr_current_option = None
|
||||
self._optimistic = config[CONF_OPTIMISTIC]
|
||||
self._attr_options = config[CONF_OPTIONS]
|
||||
|
||||
|
||||
@@ -98,13 +98,13 @@ def validate_options(conf: ConfigType) -> ConfigType:
|
||||
|
||||
_PLATFORM_SCHEMA_BASE = MQTT_RO_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
|
||||
vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int,
|
||||
vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean,
|
||||
vol.Optional(CONF_LAST_RESET_TOPIC): valid_subscribe_topic,
|
||||
vol.Optional(CONF_LAST_RESET_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_STATE_CLASS): STATE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_STATE_CLASS): vol.Any(STATE_CLASSES_SCHEMA, None),
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
}
|
||||
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
|
||||
|
||||
@@ -62,7 +62,7 @@ PLATFORM_SCHEMA_MODERN = MQTT_RW_SCHEMA.extend(
|
||||
vol.Optional(CONF_STATE_OFF): cv.string,
|
||||
vol.Optional(CONF_STATE_ON): cv.string,
|
||||
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
|
||||
}
|
||||
).extend(MQTT_ENTITY_COMMON_SCHEMA.schema)
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ CONF_TITLE = "title"
|
||||
PLATFORM_SCHEMA_MODERN = MQTT_RO_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_COMMAND_TOPIC): valid_publish_topic,
|
||||
vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_DEVICE_CLASS): vol.Any(DEVICE_CLASSES_SCHEMA, None),
|
||||
vol.Optional(CONF_ENTITY_PICTURE): cv.string,
|
||||
vol.Optional(CONF_LATEST_VERSION_TEMPLATE): cv.template,
|
||||
vol.Optional(CONF_LATEST_VERSION_TOPIC): valid_subscribe_topic,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Nanoleaf",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/nanoleaf",
|
||||
"requirements": ["aionanoleaf==0.2.0"],
|
||||
"requirements": ["aionanoleaf==0.2.1"],
|
||||
"zeroconf": ["_nanoleafms._tcp.local.", "_nanoleafapi._tcp.local."],
|
||||
"homekit": {
|
||||
"models": ["NL29", "NL42", "NL47", "NL48", "NL52", "NL59"]
|
||||
|
||||
@@ -58,8 +58,8 @@ SENSOR_TYPES = {
|
||||
key="signal",
|
||||
name="signal strength",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
icon="mdi:wifi",
|
||||
),
|
||||
"ssid": SensorEntityDescription(
|
||||
key="ssid",
|
||||
|
||||
@@ -213,7 +213,7 @@ class NumberDeviceClass(StrEnum):
|
||||
POWER_FACTOR = "power_factor"
|
||||
"""Power factor.
|
||||
|
||||
Unit of measurement: `%`
|
||||
Unit of measurement: `%`, `None`
|
||||
"""
|
||||
|
||||
POWER = "power"
|
||||
|
||||
@@ -31,7 +31,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
LOGGER,
|
||||
)
|
||||
from .coordinator import InvalidApiKeyMonitor, OpenUvCoordinator
|
||||
from .coordinator import OpenUvCoordinator
|
||||
|
||||
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
@@ -45,6 +45,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry.data.get(CONF_LONGITUDE, hass.config.longitude),
|
||||
altitude=entry.data.get(CONF_ELEVATION, hass.config.elevation),
|
||||
session=websession,
|
||||
check_status_before_request=True,
|
||||
)
|
||||
|
||||
async def async_update_protection_data() -> dict[str, Any]:
|
||||
@@ -53,16 +54,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
high = entry.options.get(CONF_TO_WINDOW, DEFAULT_TO_WINDOW)
|
||||
return await client.uv_protection_window(low=low, high=high)
|
||||
|
||||
invalid_api_key_monitor = InvalidApiKeyMonitor(hass, entry)
|
||||
|
||||
coordinators: dict[str, OpenUvCoordinator] = {
|
||||
coordinator_name: OpenUvCoordinator(
|
||||
hass,
|
||||
entry=entry,
|
||||
name=coordinator_name,
|
||||
latitude=client.latitude,
|
||||
longitude=client.longitude,
|
||||
update_method=update_method,
|
||||
invalid_api_key_monitor=invalid_api_key_monitor,
|
||||
)
|
||||
for coordinator_name, update_method in (
|
||||
(DATA_UV, client.uv_index),
|
||||
@@ -70,16 +69,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
}
|
||||
|
||||
# We disable the client's request retry abilities here to avoid a lengthy (and
|
||||
# blocking) startup; then, if the initial update is successful, we re-enable client
|
||||
# request retries:
|
||||
client.disable_request_retries()
|
||||
init_tasks = [
|
||||
coordinator.async_config_entry_first_refresh()
|
||||
for coordinator in coordinators.values()
|
||||
]
|
||||
await asyncio.gather(*init_tasks)
|
||||
client.enable_request_retries()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinators
|
||||
|
||||
@@ -103,7 +103,6 @@ class OpenUvFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Verify the credentials and create/re-auth the entry."""
|
||||
websession = aiohttp_client.async_get_clientsession(self.hass)
|
||||
client = Client(data.api_key, 0, 0, session=websession)
|
||||
client.disable_request_retries()
|
||||
|
||||
try:
|
||||
await client.uv_index()
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
"""Define an update coordinator for OpenUV."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any, cast
|
||||
|
||||
from pyopenuv.errors import InvalidApiKeyError, OpenUvError
|
||||
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.debounce import Debouncer
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
@@ -18,64 +17,6 @@ from .const import LOGGER
|
||||
DEFAULT_DEBOUNCER_COOLDOWN_SECONDS = 15 * 60
|
||||
|
||||
|
||||
class InvalidApiKeyMonitor:
|
||||
"""Define a monitor for failed API calls (due to bad keys) across coordinators."""
|
||||
|
||||
DEFAULT_FAILED_API_CALL_THRESHOLD = 5
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Initialize."""
|
||||
self._count = 1
|
||||
self._lock = asyncio.Lock()
|
||||
self._reauth_flow_manager = ReauthFlowManager(hass, entry)
|
||||
self.entry = entry
|
||||
|
||||
async def async_increment(self) -> None:
|
||||
"""Increment the counter."""
|
||||
async with self._lock:
|
||||
self._count += 1
|
||||
if self._count > self.DEFAULT_FAILED_API_CALL_THRESHOLD:
|
||||
LOGGER.info("Starting reauth after multiple failed API calls")
|
||||
self._reauth_flow_manager.start_reauth()
|
||||
|
||||
async def async_reset(self) -> None:
|
||||
"""Reset the counter."""
|
||||
async with self._lock:
|
||||
self._count = 0
|
||||
self._reauth_flow_manager.cancel_reauth()
|
||||
|
||||
|
||||
class ReauthFlowManager:
|
||||
"""Define an OpenUV reauth flow manager."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Initialize."""
|
||||
self.entry = entry
|
||||
self.hass = hass
|
||||
|
||||
@callback
|
||||
def _get_active_reauth_flow(self) -> FlowResult | None:
|
||||
"""Get an active reauth flow (if it exists)."""
|
||||
return next(
|
||||
iter(self.entry.async_get_active_flows(self.hass, {SOURCE_REAUTH})),
|
||||
None,
|
||||
)
|
||||
|
||||
@callback
|
||||
def cancel_reauth(self) -> None:
|
||||
"""Cancel a reauth flow (if appropriate)."""
|
||||
if reauth_flow := self._get_active_reauth_flow():
|
||||
LOGGER.debug("API seems to have recovered; canceling reauth flow")
|
||||
self.hass.config_entries.flow.async_abort(reauth_flow["flow_id"])
|
||||
|
||||
@callback
|
||||
def start_reauth(self) -> None:
|
||||
"""Start a reauth flow (if appropriate)."""
|
||||
if not self._get_active_reauth_flow():
|
||||
LOGGER.debug("Multiple API failures in a row; starting reauth flow")
|
||||
self.entry.async_start_reauth(self.hass)
|
||||
|
||||
|
||||
class OpenUvCoordinator(DataUpdateCoordinator):
|
||||
"""Define an OpenUV data coordinator."""
|
||||
|
||||
@@ -86,11 +27,11 @@ class OpenUvCoordinator(DataUpdateCoordinator):
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
*,
|
||||
entry: ConfigEntry,
|
||||
name: str,
|
||||
latitude: str,
|
||||
longitude: str,
|
||||
update_method: Callable[[], Awaitable[dict[str, Any]]],
|
||||
invalid_api_key_monitor: InvalidApiKeyMonitor,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
@@ -106,7 +47,7 @@ class OpenUvCoordinator(DataUpdateCoordinator):
|
||||
),
|
||||
)
|
||||
|
||||
self._invalid_api_key_monitor = invalid_api_key_monitor
|
||||
self._entry = entry
|
||||
self.latitude = latitude
|
||||
self.longitude = longitude
|
||||
|
||||
@@ -115,10 +56,18 @@ class OpenUvCoordinator(DataUpdateCoordinator):
|
||||
try:
|
||||
data = await self.update_method()
|
||||
except InvalidApiKeyError as err:
|
||||
await self._invalid_api_key_monitor.async_increment()
|
||||
raise UpdateFailed(str(err)) from err
|
||||
raise ConfigEntryAuthFailed("Invalid API key") from err
|
||||
except OpenUvError as err:
|
||||
raise UpdateFailed(str(err)) from err
|
||||
|
||||
await self._invalid_api_key_monitor.async_reset()
|
||||
# OpenUV uses HTTP 403 to indicate both an invalid API key and an API key that
|
||||
# has hit its daily/monthly limit; both cases will result in a reauth flow. If
|
||||
# coordinator update succeeds after a reauth flow has been started, terminate
|
||||
# it:
|
||||
if reauth_flow := next(
|
||||
iter(self._entry.async_get_active_flows(self.hass, {SOURCE_REAUTH})),
|
||||
None,
|
||||
):
|
||||
self.hass.config_entries.flow.async_abort(reauth_flow["flow_id"])
|
||||
|
||||
return cast(dict[str, Any], data["result"])
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "OpenUV",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/openuv",
|
||||
"requirements": ["pyopenuv==2022.04.0"],
|
||||
"requirements": ["pyopenuv==2023.01.0"],
|
||||
"codeowners": ["@bachya"],
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyopenuv"],
|
||||
|
||||
@@ -210,7 +210,7 @@ class PhilipsTVMediaPlayer(
|
||||
async def async_media_play_pause(self) -> None:
|
||||
"""Send pause command to media player."""
|
||||
if self._tv.quirk_playpause_spacebar:
|
||||
await self._tv.sendUnicode(" ")
|
||||
await self._tv.sendKey("Confirm")
|
||||
else:
|
||||
await self._tv.sendKey("PlayPause")
|
||||
await self._async_update_soon()
|
||||
@@ -509,6 +509,8 @@ class PhilipsTVMediaPlayer(
|
||||
self._media_title = self._sources.get(self._tv.source_id)
|
||||
self._media_channel = None
|
||||
|
||||
self._attr_assumed_state = True
|
||||
|
||||
@callback
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Handle updated data from the coordinator."""
|
||||
|
||||
@@ -6,12 +6,10 @@ from aiopurpleair.models.sensors import SensorModel
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import ATTR_LATITUDE, ATTR_LONGITUDE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
import homeassistant.helpers.device_registry as dr
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .config_flow import async_remove_sensor_by_device_id
|
||||
from .const import CONF_LAST_UPDATE_SENSOR_ADD, DOMAIN
|
||||
from .const import DOMAIN
|
||||
from .coordinator import PurpleAirDataUpdateCoordinator
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
@@ -32,26 +30,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
async def async_handle_entry_update(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle an options update."""
|
||||
if entry.options.get(CONF_LAST_UPDATE_SENSOR_ADD) is True:
|
||||
# If the last options update was to add a sensor, we reload the config entry:
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_remove_config_entry_device(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry
|
||||
) -> bool:
|
||||
"""Remove a config entry from a device."""
|
||||
new_entry_options = async_remove_sensor_by_device_id(
|
||||
hass,
|
||||
config_entry,
|
||||
device_entry.id,
|
||||
# remove_device is set to False because in this instance, the device has
|
||||
# already been removed:
|
||||
remove_device=False,
|
||||
)
|
||||
return hass.config_entries.async_update_entry(
|
||||
config_entry, options=new_entry_options
|
||||
)
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Config flow for PurpleAir integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Mapping
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass, field
|
||||
@@ -14,13 +15,15 @@ import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import Event, HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers import (
|
||||
aiohttp_client,
|
||||
config_validation as cv,
|
||||
device_registry as dr,
|
||||
entity_registry as er,
|
||||
)
|
||||
from homeassistant.helpers.event import async_track_state_change_event
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectOptionDict,
|
||||
SelectSelector,
|
||||
@@ -28,7 +31,7 @@ from homeassistant.helpers.selector import (
|
||||
SelectSelectorMode,
|
||||
)
|
||||
|
||||
from .const import CONF_LAST_UPDATE_SENSOR_ADD, CONF_SENSOR_INDICES, DOMAIN, LOGGER
|
||||
from .const import CONF_SENSOR_INDICES, DOMAIN, LOGGER
|
||||
|
||||
CONF_DISTANCE = "distance"
|
||||
CONF_NEARBY_SENSOR_OPTIONS = "nearby_sensor_options"
|
||||
@@ -117,50 +120,6 @@ def async_get_remove_sensor_schema(sensors: list[SelectOptionDict]) -> vol.Schem
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_sensor_index(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, device_entry: dr.DeviceEntry
|
||||
) -> int:
|
||||
"""Get the sensor index related to a config and device entry.
|
||||
|
||||
Note that this method expects that there will always be a single sensor index per
|
||||
DeviceEntry.
|
||||
"""
|
||||
[sensor_index] = [
|
||||
sensor_index
|
||||
for sensor_index in config_entry.options[CONF_SENSOR_INDICES]
|
||||
if (DOMAIN, str(sensor_index)) in device_entry.identifiers
|
||||
]
|
||||
|
||||
return cast(int, sensor_index)
|
||||
|
||||
|
||||
@callback
|
||||
def async_remove_sensor_by_device_id(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
device_id: str,
|
||||
*,
|
||||
remove_device: bool = True,
|
||||
) -> dict[str, Any]:
|
||||
"""Remove a sensor and return update config entry options."""
|
||||
device_registry = dr.async_get(hass)
|
||||
device_entry = device_registry.async_get(device_id)
|
||||
assert device_entry
|
||||
|
||||
removed_sensor_index = async_get_sensor_index(hass, config_entry, device_entry)
|
||||
options = deepcopy({**config_entry.options})
|
||||
options[CONF_LAST_UPDATE_SENSOR_ADD] = False
|
||||
options[CONF_SENSOR_INDICES].remove(removed_sensor_index)
|
||||
|
||||
if remove_device:
|
||||
device_registry.async_update_device(
|
||||
device_entry.id, remove_config_entry_id=config_entry.entry_id
|
||||
)
|
||||
|
||||
return options
|
||||
|
||||
|
||||
@dataclass
|
||||
class ValidationResult:
|
||||
"""Define a validation result."""
|
||||
@@ -407,7 +366,6 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
return self.async_abort(reason="already_configured")
|
||||
|
||||
options = deepcopy({**self.config_entry.options})
|
||||
options[CONF_LAST_UPDATE_SENSOR_ADD] = True
|
||||
options[CONF_SENSOR_INDICES].append(sensor_index)
|
||||
return self.async_create_entry(title="", data=options)
|
||||
|
||||
@@ -432,8 +390,50 @@ class PurpleAirOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
),
|
||||
)
|
||||
|
||||
new_entry_options = async_remove_sensor_by_device_id(
|
||||
self.hass, self.config_entry, user_input[CONF_SENSOR_DEVICE_ID]
|
||||
device_registry = dr.async_get(self.hass)
|
||||
entity_registry = er.async_get(self.hass)
|
||||
|
||||
device_id = user_input[CONF_SENSOR_DEVICE_ID]
|
||||
device_entry = cast(dr.DeviceEntry, device_registry.async_get(device_id))
|
||||
|
||||
# Determine the entity entries that belong to this device.
|
||||
entity_entries = er.async_entries_for_device(
|
||||
entity_registry, device_id, include_disabled_entities=True
|
||||
)
|
||||
|
||||
return self.async_create_entry(title="", data=new_entry_options)
|
||||
device_entities_removed_event = asyncio.Event()
|
||||
|
||||
@callback
|
||||
def async_device_entity_state_changed(_: Event) -> None:
|
||||
"""Listen and respond when all device entities are removed."""
|
||||
if all(
|
||||
self.hass.states.get(entity_entry.entity_id) is None
|
||||
for entity_entry in entity_entries
|
||||
):
|
||||
device_entities_removed_event.set()
|
||||
|
||||
# Track state changes for this device's entities and when they're removed,
|
||||
# finish the flow:
|
||||
cancel_state_track = async_track_state_change_event(
|
||||
self.hass,
|
||||
[entity_entry.entity_id for entity_entry in entity_entries],
|
||||
async_device_entity_state_changed,
|
||||
)
|
||||
device_registry.async_update_device(
|
||||
device_id, remove_config_entry_id=self.config_entry.entry_id
|
||||
)
|
||||
await device_entities_removed_event.wait()
|
||||
|
||||
# Once we're done, we can cancel the state change tracker callback:
|
||||
cancel_state_track()
|
||||
|
||||
# Build new config entry options:
|
||||
removed_sensor_index = next(
|
||||
sensor_index
|
||||
for sensor_index in self.config_entry.options[CONF_SENSOR_INDICES]
|
||||
if (DOMAIN, str(sensor_index)) in device_entry.identifiers
|
||||
)
|
||||
options = deepcopy({**self.config_entry.options})
|
||||
options[CONF_SENSOR_INDICES].remove(removed_sensor_index)
|
||||
|
||||
return self.async_create_entry(title="", data=options)
|
||||
|
||||
@@ -5,6 +5,5 @@ DOMAIN = "purpleair"
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
||||
|
||||
CONF_LAST_UPDATE_SENSOR_ADD = "last_update_sensor_add"
|
||||
CONF_READ_KEY = "read_key"
|
||||
CONF_SENSOR_INDICES = "sensor_indices"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Reolink IP NVR/camera",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/reolink",
|
||||
"requirements": ["reolink-aio==0.1.1"],
|
||||
"requirements": ["reolink-aio==0.1.3"],
|
||||
"codeowners": ["@starkillerOG"],
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["reolink-aio"]
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "sense",
|
||||
"name": "Sense",
|
||||
"documentation": "https://www.home-assistant.io/integrations/sense",
|
||||
"requirements": ["sense_energy==0.11.0"],
|
||||
"requirements": ["sense_energy==0.11.1"],
|
||||
"codeowners": ["@kbickar"],
|
||||
"config_flow": true,
|
||||
"dhcp": [
|
||||
|
||||
@@ -309,7 +309,7 @@ class SensorDeviceClass(StrEnum):
|
||||
POWER_FACTOR = "power_factor"
|
||||
"""Power factor.
|
||||
|
||||
Unit of measurement: `%`
|
||||
Unit of measurement: `%`, `None`
|
||||
"""
|
||||
|
||||
POWER = "power"
|
||||
@@ -521,7 +521,7 @@ DEVICE_CLASS_UNITS: dict[SensorDeviceClass, set[type[StrEnum] | str | None]] = {
|
||||
SensorDeviceClass.PM1: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
|
||||
SensorDeviceClass.PM10: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
|
||||
SensorDeviceClass.PM25: {CONCENTRATION_MICROGRAMS_PER_CUBIC_METER},
|
||||
SensorDeviceClass.POWER_FACTOR: {PERCENTAGE},
|
||||
SensorDeviceClass.POWER_FACTOR: {PERCENTAGE, None},
|
||||
SensorDeviceClass.POWER: {UnitOfPower.WATT, UnitOfPower.KILO_WATT},
|
||||
SensorDeviceClass.PRECIPITATION: set(UnitOfPrecipitationDepth),
|
||||
SensorDeviceClass.PRECIPITATION_INTENSITY: set(UnitOfVolumetricFlux),
|
||||
|
||||
@@ -53,6 +53,8 @@ BLE_SCANNER_OPTIONS = [
|
||||
selector.SelectOptionDict(value=BLEScannerMode.PASSIVE, label="Passive"),
|
||||
]
|
||||
|
||||
INTERNAL_WIFI_AP_IP = "192.168.33.1"
|
||||
|
||||
|
||||
async def validate_input(
|
||||
hass: HomeAssistant,
|
||||
@@ -217,7 +219,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
current_entry := await self.async_set_unique_id(mac)
|
||||
) and current_entry.data[CONF_HOST] == host:
|
||||
await async_reconnect_soon(self.hass, current_entry)
|
||||
self._abort_if_unique_id_configured({CONF_HOST: host})
|
||||
if host == INTERNAL_WIFI_AP_IP:
|
||||
# If the device is broadcasting the internal wifi ap ip
|
||||
# we can't connect to it, so we should not update the
|
||||
# entry with the new host as it will be unreachable
|
||||
#
|
||||
# This is a workaround for a bug in the firmware 0.12 (and older?)
|
||||
# which should be removed once the firmware is fixed
|
||||
# and the old version is no longer in use
|
||||
self._abort_if_unique_id_configured()
|
||||
else:
|
||||
self._abort_if_unique_id_configured({CONF_HOST: host})
|
||||
|
||||
async def async_step_zeroconf(
|
||||
self, discovery_info: zeroconf.ZeroconfServiceInfo
|
||||
|
||||
@@ -509,7 +509,8 @@ class ShellyRpcCoordinator(DataUpdateCoordinator[None]):
|
||||
This will be executed on connect or when the config entry
|
||||
is updated.
|
||||
"""
|
||||
await self._async_connect_ble_scanner()
|
||||
if not self.entry.data.get(CONF_SLEEP_PERIOD):
|
||||
await self._async_connect_ble_scanner()
|
||||
|
||||
async def _async_connect_ble_scanner(self) -> None:
|
||||
"""Connect BLE scanner."""
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Shelly",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/shelly",
|
||||
"requirements": ["aioshelly==5.2.0"],
|
||||
"requirements": ["aioshelly==5.2.1"],
|
||||
"dependencies": ["bluetooth", "http"],
|
||||
"zeroconf": [
|
||||
{
|
||||
|
||||
@@ -131,7 +131,6 @@ EV_SENSORS = [
|
||||
key=sc.EV_TIME_TO_FULLY_CHARGED_UTC,
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
name="EV time to full charge",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -166,6 +166,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle the SwitchBot API auth step."""
|
||||
errors = {}
|
||||
assert self._discovered_adv is not None
|
||||
description_placeholders = {}
|
||||
if user_input is not None:
|
||||
try:
|
||||
key_details = await self.hass.async_add_executor_job(
|
||||
@@ -176,8 +177,10 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
)
|
||||
except SwitchbotAccountConnectionError as ex:
|
||||
raise AbortFlow("cannot_connect") from ex
|
||||
except SwitchbotAuthenticationError:
|
||||
except SwitchbotAuthenticationError as ex:
|
||||
_LOGGER.debug("Authentication failed: %s", ex, exc_info=True)
|
||||
errors = {"base": "auth_failed"}
|
||||
description_placeholders = {"error_detail": str(ex)}
|
||||
else:
|
||||
return await self.async_step_lock_key(key_details)
|
||||
|
||||
@@ -195,6 +198,7 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
),
|
||||
description_placeholders={
|
||||
"name": name_from_discovery(self._discovered_adv),
|
||||
**description_placeholders,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"domain": "switchbot",
|
||||
"name": "SwitchBot",
|
||||
"documentation": "https://www.home-assistant.io/integrations/switchbot",
|
||||
"requirements": ["PySwitchbot==0.36.1"],
|
||||
"requirements": ["PySwitchbot==0.36.3"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth"],
|
||||
"codeowners": [
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
}
|
||||
},
|
||||
"lock_auth": {
|
||||
"description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your locks encryption key.",
|
||||
"description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your locks encryption key. Usernames and passwords are case sensitive.",
|
||||
"data": {
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
@@ -40,7 +40,7 @@
|
||||
},
|
||||
"error": {
|
||||
"encryption_key_invalid": "Key ID or Encryption key is invalid",
|
||||
"auth_failed": "Authentication failed"
|
||||
"auth_failed": "Authentication failed: {error_detail}"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"error": {
|
||||
"auth_failed": "Authentication failed",
|
||||
"auth_failed": "Authentication failed: {error_detail}",
|
||||
"encryption_key_invalid": "Key ID or Encryption key is invalid"
|
||||
},
|
||||
"flow_title": "{name} ({address})",
|
||||
@@ -21,7 +21,7 @@
|
||||
"password": "Password",
|
||||
"username": "Username"
|
||||
},
|
||||
"description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your locks encryption key."
|
||||
"description": "Please provide your SwitchBot app username and password. This data won't be saved and only used to retrieve your locks encryption key. Usernames and passwords are case sensitive."
|
||||
},
|
||||
"lock_choose_method": {
|
||||
"description": "A SwitchBot lock can be set up in Home Assistant in two different ways.\n\nYou can enter the key id and encryption key yourself, or Home Assistant can import them from your SwitchBot account.",
|
||||
@@ -47,18 +47,7 @@
|
||||
"data": {
|
||||
"address": "Device address"
|
||||
}
|
||||
},
|
||||
"lock_key": {
|
||||
"description": "The {name} device requires encryption key, details on how to obtain it can be found in the documentation.",
|
||||
"data": {
|
||||
"key_id": "Key ID",
|
||||
"encryption_key": "Encryption key"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"key_id_invalid": "Key ID or Encryption key is invalid",
|
||||
"encryption_key_invalid": "Key ID or Encryption key is invalid"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
@@ -70,4 +59,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Tasmota",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/tasmota",
|
||||
"requirements": ["hatasmota==0.6.1"],
|
||||
"requirements": ["hatasmota==0.6.3"],
|
||||
"dependencies": ["mqtt"],
|
||||
"mqtt": ["tasmota/discovery/#"],
|
||||
"codeowners": ["@emontnemery"],
|
||||
|
||||
@@ -21,6 +21,7 @@ from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
POWER_VOLT_AMPERE_REACTIVE,
|
||||
SIGNAL_STRENGTH_DECIBELS,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
UnitOfApparentPower,
|
||||
@@ -217,8 +218,10 @@ SENSOR_UNIT_MAP = {
|
||||
hc.LIGHT_LUX: LIGHT_LUX,
|
||||
hc.MASS_KILOGRAMS: UnitOfMass.KILOGRAMS,
|
||||
hc.PERCENTAGE: PERCENTAGE,
|
||||
hc.POWER_FACTOR: None,
|
||||
hc.POWER_WATT: UnitOfPower.WATT,
|
||||
hc.PRESSURE_HPA: UnitOfPressure.HPA,
|
||||
hc.REACTIVE_POWER: POWER_VOLT_AMPERE_REACTIVE,
|
||||
hc.SIGNAL_STRENGTH_DECIBELS: SIGNAL_STRENGTH_DECIBELS,
|
||||
hc.SIGNAL_STRENGTH_DECIBELS_MILLIWATT: SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
hc.SPEED_KILOMETERS_PER_HOUR: UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"domain": "tibber",
|
||||
"name": "Tibber",
|
||||
"documentation": "https://www.home-assistant.io/integrations/tibber",
|
||||
"requirements": ["pyTibber==0.26.7"],
|
||||
"requirements": ["pyTibber==0.26.8"],
|
||||
"codeowners": ["@danielhiversen"],
|
||||
"quality_scale": "silver",
|
||||
"config_flow": true,
|
||||
|
||||
@@ -65,6 +65,7 @@ class UnifiEntity(Entity, Generic[HandlerT, DataT]):
|
||||
self.entity_description = description
|
||||
|
||||
self._removed = False
|
||||
self._write_state = False
|
||||
|
||||
self._attr_available = description.available_fn(controller, obj_id)
|
||||
self._attr_device_info = description.device_info_fn(controller.api, obj_id)
|
||||
@@ -117,9 +118,14 @@ class UnifiEntity(Entity, Generic[HandlerT, DataT]):
|
||||
self.hass.async_create_task(self.remove_item({self._obj_id}))
|
||||
return
|
||||
|
||||
self._attr_available = description.available_fn(self.controller, self._obj_id)
|
||||
if (
|
||||
available := description.available_fn(self.controller, self._obj_id)
|
||||
) != self.available:
|
||||
self._attr_available = available
|
||||
self._write_state = True
|
||||
self.async_update_state(event, obj_id)
|
||||
self.async_write_ha_state()
|
||||
if self._write_state:
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def async_signal_reachable_callback(self) -> None:
|
||||
|
||||
@@ -217,6 +217,7 @@ class UnifiSensorEntity(SensorEntity, Generic[_HandlerT, _DataT]):
|
||||
self.async_on_remove(
|
||||
handler.subscribe(
|
||||
self.async_signalling_callback,
|
||||
id_filter=self._obj_id,
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
@@ -253,11 +254,19 @@ class UnifiSensorEntity(SensorEntity, Generic[_HandlerT, _DataT]):
|
||||
self.hass.async_create_task(self.remove_item({self._obj_id}))
|
||||
return
|
||||
|
||||
update_state = False
|
||||
|
||||
obj = description.object_fn(self.controller.api, self._obj_id)
|
||||
if (value := description.value_fn(self.controller, obj)) != self.native_value:
|
||||
self._attr_native_value = value
|
||||
self._attr_available = description.available_fn(self.controller, self._obj_id)
|
||||
self.async_write_ha_state()
|
||||
update_state = True
|
||||
if (
|
||||
available := description.available_fn(self.controller, self._obj_id)
|
||||
) != self.available:
|
||||
self._attr_available = available
|
||||
update_state = True
|
||||
if update_state:
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def async_signal_reachable_callback(self) -> None:
|
||||
|
||||
@@ -361,6 +361,7 @@ class UnifiSwitchEntity(SwitchEntity, Generic[_HandlerT, _DataT]):
|
||||
self.async_on_remove(
|
||||
handler.subscribe(
|
||||
self.async_signalling_callback,
|
||||
id_filter=self._obj_id,
|
||||
)
|
||||
)
|
||||
self.async_on_remove(
|
||||
@@ -410,11 +411,20 @@ class UnifiSwitchEntity(SwitchEntity, Generic[_HandlerT, _DataT]):
|
||||
self.hass.async_create_task(self.remove_item({self._obj_id}))
|
||||
return
|
||||
|
||||
update_state = False
|
||||
|
||||
if not description.only_event_for_state_change:
|
||||
obj = description.object_fn(self.controller.api, self._obj_id)
|
||||
self._attr_is_on = description.is_on_fn(self.controller.api, obj)
|
||||
self._attr_available = description.available_fn(self.controller, self._obj_id)
|
||||
self.async_write_ha_state()
|
||||
if (is_on := description.is_on_fn(self.controller.api, obj)) != self.is_on:
|
||||
self._attr_is_on = is_on
|
||||
update_state = True
|
||||
if (
|
||||
available := description.available_fn(self.controller, self._obj_id)
|
||||
) != self.available:
|
||||
self._attr_available = available
|
||||
update_state = True
|
||||
if update_state:
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def async_signal_reachable_callback(self) -> None:
|
||||
|
||||
@@ -163,6 +163,12 @@ class UnifiDeviceUpdateEntity(UnifiEntity[HandlerT, DataT], UpdateEntity):
|
||||
description = self.entity_description
|
||||
|
||||
obj = description.object_fn(self.controller.api, self._obj_id)
|
||||
self._attr_in_progress = description.state_fn(self.controller.api, obj)
|
||||
if (
|
||||
in_progress := description.state_fn(self.controller.api, obj)
|
||||
) != self.in_progress:
|
||||
self._attr_in_progress = in_progress
|
||||
self._write_state = True
|
||||
self._attr_installed_version = obj.version
|
||||
self._attr_latest_version = obj.upgrade_to_firmware or obj.version
|
||||
if self.installed_version != self.latest_version:
|
||||
self._write_state = True
|
||||
|
||||
@@ -337,7 +337,7 @@ EVENT_SENSORS: tuple[ProtectBinaryEventEntityDescription, ...] = (
|
||||
name="Doorbell",
|
||||
device_class=BinarySensorDeviceClass.OCCUPANCY,
|
||||
icon="mdi:doorbell-video",
|
||||
ufp_required_field="feature_flags.has_chime",
|
||||
ufp_required_field="feature_flags.is_doorbell",
|
||||
ufp_value="is_ringing",
|
||||
ufp_event_obj="last_ring_event",
|
||||
),
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/unifiprotect",
|
||||
"requirements": ["pyunifiprotect==4.5.2", "unifi-discovery==1.1.7"],
|
||||
"requirements": ["pyunifiprotect==4.6.1", "unifi-discovery==1.1.7"],
|
||||
"dependencies": ["http", "repairs"],
|
||||
"codeowners": ["@briis", "@AngellusMortis", "@bdraco"],
|
||||
"quality_scale": "platinum",
|
||||
|
||||
@@ -770,7 +770,7 @@ class ProtectMediaSource(MediaSource):
|
||||
if camera is None:
|
||||
raise BrowseError(f"Unknown Camera ID: {camera_id}")
|
||||
name = camera.name or camera.market_name or camera.type
|
||||
is_doorbell = camera.feature_flags.has_chime
|
||||
is_doorbell = camera.feature_flags.is_doorbell
|
||||
has_smart = camera.feature_flags.has_smart_detect
|
||||
|
||||
thumbnail_url: str | None = None
|
||||
|
||||
@@ -206,7 +206,7 @@ CAMERA_SENSORS: tuple[ProtectSensorEntityDescription, ...] = (
|
||||
name="Last Doorbell Ring",
|
||||
device_class=SensorDeviceClass.TIMESTAMP,
|
||||
icon="mdi:doorbell-video",
|
||||
ufp_required_field="feature_flags.has_chime",
|
||||
ufp_required_field="feature_flags.is_doorbell",
|
||||
ufp_value="last_ring",
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "LG webOS Smart TV",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/webostv",
|
||||
"requirements": ["aiowebostv==0.2.1"],
|
||||
"requirements": ["aiowebostv==0.3.0"],
|
||||
"codeowners": ["@bendavid", "@thecode"],
|
||||
"ssdp": [{ "st": "urn:lge-com:service:webos-second-screen:1" }],
|
||||
"quality_scale": "platinum",
|
||||
|
||||
@@ -17,6 +17,7 @@ from zigpy.application import ControllerApplication
|
||||
from zigpy.config import CONF_DEVICE
|
||||
import zigpy.device
|
||||
import zigpy.endpoint
|
||||
import zigpy.exceptions
|
||||
import zigpy.group
|
||||
from zigpy.types.named import EUI64
|
||||
|
||||
@@ -24,6 +25,7 @@ from homeassistant import __path__ as HOMEASSISTANT_PATH
|
||||
from homeassistant.components.system_log import LogEntry, _figure_out_source
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.entity import DeviceInfo
|
||||
@@ -172,6 +174,8 @@ class ZHAGateway:
|
||||
self.application_controller = await app_controller_cls.new(
|
||||
app_config, auto_form=True, start_radio=True
|
||||
)
|
||||
except zigpy.exceptions.TransientConnectionError as exc:
|
||||
raise ConfigEntryNotReady from exc
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
_LOGGER.warning(
|
||||
"Couldn't start %s coordinator (attempt %s of %s)",
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/zha",
|
||||
"requirements": [
|
||||
"bellows==0.34.5",
|
||||
"bellows==0.34.6",
|
||||
"pyserial==3.5",
|
||||
"pyserial-asyncio==0.6",
|
||||
"zha-quirks==0.0.90",
|
||||
"zigpy-deconz==0.19.2",
|
||||
"zigpy==0.52.3",
|
||||
"zigpy==0.53.0",
|
||||
"zigpy-xbee==0.16.2",
|
||||
"zigpy-zigate==0.10.3",
|
||||
"zigpy-znp==0.9.2"
|
||||
|
||||
@@ -755,7 +755,6 @@ class RSSISensor(Sensor, id_suffix="rssi"):
|
||||
"""RSSI sensor for a device."""
|
||||
|
||||
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
|
||||
_attr_device_class: SensorDeviceClass = SensorDeviceClass.SIGNAL_STRENGTH
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
_attr_entity_registry_enabled_default = False
|
||||
_attr_should_poll = True # BaseZhaEntity defaults to False
|
||||
|
||||
@@ -34,6 +34,7 @@ from zwave_js_server.const.command_class.multilevel_sensor import (
|
||||
PRESSURE_SENSORS,
|
||||
SIGNAL_STRENGTH_SENSORS,
|
||||
TEMPERATURE_SENSORS,
|
||||
UNIT_A_WEIGHTED_DECIBELS,
|
||||
UNIT_AMPERE as SENSOR_UNIT_AMPERE,
|
||||
UNIT_BTU_H,
|
||||
UNIT_CELSIUS,
|
||||
@@ -52,6 +53,7 @@ from zwave_js_server.const.command_class.multilevel_sensor import (
|
||||
UNIT_INCHES_PER_HOUR,
|
||||
UNIT_KILOGRAM,
|
||||
UNIT_KILOHERTZ,
|
||||
UNIT_KILOPASCAL,
|
||||
UNIT_LITER,
|
||||
UNIT_LUX,
|
||||
UNIT_M_S,
|
||||
@@ -69,6 +71,7 @@ from zwave_js_server.const.command_class.multilevel_sensor import (
|
||||
UNIT_RSSI,
|
||||
UNIT_SECOND,
|
||||
UNIT_SYSTOLIC,
|
||||
UNIT_UV_INDEX,
|
||||
UNIT_VOLT as SENSOR_UNIT_VOLT,
|
||||
UNIT_WATT as SENSOR_UNIT_WATT,
|
||||
UNIT_WATT_PER_SQUARE_METER,
|
||||
@@ -94,8 +97,8 @@ from homeassistant.const import (
|
||||
DEGREE,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
UV_INDEX,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
@@ -105,6 +108,7 @@ from homeassistant.const import (
|
||||
UnitOfMass,
|
||||
UnitOfPower,
|
||||
UnitOfPressure,
|
||||
UnitOfSoundPressure,
|
||||
UnitOfSpeed,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
@@ -134,7 +138,7 @@ from .const import (
|
||||
)
|
||||
from .helpers import ZwaveValueID
|
||||
|
||||
METER_DEVICE_CLASS_MAP: dict[str, set[MeterScaleType]] = {
|
||||
METER_DEVICE_CLASS_MAP: dict[str, list[MeterScaleType]] = {
|
||||
ENTITY_DESC_KEY_CURRENT: CURRENT_METER_TYPES,
|
||||
ENTITY_DESC_KEY_VOLTAGE: VOLTAGE_METER_TYPES,
|
||||
ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING: ENERGY_TOTAL_INCREASING_METER_TYPES,
|
||||
@@ -142,7 +146,7 @@ METER_DEVICE_CLASS_MAP: dict[str, set[MeterScaleType]] = {
|
||||
ENTITY_DESC_KEY_POWER_FACTOR: POWER_FACTOR_METER_TYPES,
|
||||
}
|
||||
|
||||
MULTILEVEL_SENSOR_DEVICE_CLASS_MAP: dict[str, set[MultilevelSensorType]] = {
|
||||
MULTILEVEL_SENSOR_DEVICE_CLASS_MAP: dict[str, list[MultilevelSensorType]] = {
|
||||
ENTITY_DESC_KEY_CO: CO_SENSORS,
|
||||
ENTITY_DESC_KEY_CO2: CO2_SENSORS,
|
||||
ENTITY_DESC_KEY_CURRENT: CURRENT_SENSORS,
|
||||
@@ -156,7 +160,7 @@ MULTILEVEL_SENSOR_DEVICE_CLASS_MAP: dict[str, set[MultilevelSensorType]] = {
|
||||
ENTITY_DESC_KEY_VOLTAGE: VOLTAGE_SENSORS,
|
||||
}
|
||||
|
||||
METER_UNIT_MAP: dict[str, set[MeterScaleType]] = {
|
||||
METER_UNIT_MAP: dict[str, list[MeterScaleType]] = {
|
||||
UnitOfElectricCurrent.AMPERE: METER_UNIT_AMPERE,
|
||||
UnitOfVolume.CUBIC_FEET: UNIT_CUBIC_FEET,
|
||||
UnitOfVolume.CUBIC_METERS: METER_UNIT_CUBIC_METER,
|
||||
@@ -166,7 +170,7 @@ METER_UNIT_MAP: dict[str, set[MeterScaleType]] = {
|
||||
UnitOfPower.WATT: METER_UNIT_WATT,
|
||||
}
|
||||
|
||||
MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = {
|
||||
MULTILEVEL_SENSOR_UNIT_MAP: dict[str, list[MultilevelSensorScaleType]] = {
|
||||
UnitOfElectricCurrent.AMPERE: SENSOR_UNIT_AMPERE,
|
||||
UnitOfPower.BTU_PER_HOUR: UNIT_BTU_H,
|
||||
UnitOfTemperature.CELSIUS: UNIT_CELSIUS,
|
||||
@@ -174,17 +178,19 @@ MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = {
|
||||
UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE: UNIT_CUBIC_FEET_PER_MINUTE,
|
||||
UnitOfVolume.CUBIC_METERS: SENSOR_UNIT_CUBIC_METER,
|
||||
UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR: UNIT_CUBIC_METER_PER_HOUR,
|
||||
SIGNAL_STRENGTH_DECIBELS: UNIT_DECIBEL,
|
||||
UnitOfSoundPressure.DECIBEL: UNIT_DECIBEL,
|
||||
UnitOfSoundPressure.WEIGHTED_DECIBEL_A: UNIT_A_WEIGHTED_DECIBELS,
|
||||
DEGREE: UNIT_DEGREES,
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: {
|
||||
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: [
|
||||
*UNIT_DENSITY,
|
||||
*UNIT_MICROGRAM_PER_CUBIC_METER,
|
||||
},
|
||||
],
|
||||
UnitOfTemperature.FAHRENHEIT: UNIT_FAHRENHEIT,
|
||||
UnitOfLength.FEET: UNIT_FEET,
|
||||
UnitOfVolume.GALLONS: UNIT_GALLONS,
|
||||
UnitOfFrequency.HERTZ: UNIT_HERTZ,
|
||||
UnitOfPressure.INHG: UNIT_INCHES_OF_MERCURY,
|
||||
UnitOfPressure.KPA: UNIT_KILOPASCAL,
|
||||
UnitOfVolumetricFlux.INCHES_PER_HOUR: UNIT_INCHES_PER_HOUR,
|
||||
UnitOfMass.KILOGRAMS: UNIT_KILOGRAM,
|
||||
UnitOfFrequency.KILOHERTZ: UNIT_KILOHERTZ,
|
||||
@@ -197,7 +203,7 @@ MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = {
|
||||
UnitOfSpeed.MILES_PER_HOUR: UNIT_MPH,
|
||||
UnitOfSpeed.METERS_PER_SECOND: UNIT_M_S,
|
||||
CONCENTRATION_PARTS_PER_MILLION: UNIT_PARTS_MILLION,
|
||||
PERCENTAGE: {*UNIT_PERCENTAGE_VALUE, *UNIT_RSSI},
|
||||
PERCENTAGE: [*UNIT_PERCENTAGE_VALUE, *UNIT_RSSI],
|
||||
UnitOfMass.POUNDS: UNIT_POUNDS,
|
||||
UnitOfPressure.PSI: UNIT_POUND_PER_SQUARE_INCH,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT: UNIT_POWER_LEVEL,
|
||||
@@ -206,6 +212,7 @@ MULTILEVEL_SENSOR_UNIT_MAP: dict[str, set[MultilevelSensorScaleType]] = {
|
||||
UnitOfElectricPotential.VOLT: SENSOR_UNIT_VOLT,
|
||||
UnitOfPower.WATT: SENSOR_UNIT_WATT,
|
||||
UnitOfIrradiance.WATTS_PER_SQUARE_METER: UNIT_WATT_PER_SQUARE_METER,
|
||||
UV_INDEX: UNIT_UV_INDEX,
|
||||
}
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -319,9 +326,9 @@ class NumericSensorDataTemplate(BaseDiscoverySchemaDataTemplate):
|
||||
enum_value: MultilevelSensorType | MultilevelSensorScaleType | MeterScaleType,
|
||||
set_map: Mapping[
|
||||
str,
|
||||
set[MultilevelSensorType]
|
||||
| set[MultilevelSensorScaleType]
|
||||
| set[MeterScaleType],
|
||||
list[MultilevelSensorType]
|
||||
| list[MultilevelSensorScaleType]
|
||||
| list[MeterScaleType],
|
||||
],
|
||||
) -> str | None:
|
||||
"""Find a key in a set map that matches a given enum value."""
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Z-Wave",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/zwave_js",
|
||||
"requirements": ["pyserial==3.5", "zwave-js-server-python==0.43.1"],
|
||||
"requirements": ["pyserial==3.5", "zwave-js-server-python==0.44.0"],
|
||||
"codeowners": ["@home-assistant/z-wave"],
|
||||
"dependencies": ["usb", "http", "websocket_api"],
|
||||
"iot_class": "local_push",
|
||||
|
||||
@@ -24,6 +24,18 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
LIGHT_LUX,
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
UnitOfPower,
|
||||
UnitOfPressure,
|
||||
UnitOfTemperature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_platform
|
||||
@@ -76,98 +88,207 @@ STATUS_ICON: dict[NodeStatus, str] = {
|
||||
}
|
||||
|
||||
|
||||
ENTITY_DESCRIPTION_KEY_MAP: dict[str, SensorEntityDescription] = {
|
||||
ENTITY_DESC_KEY_BATTERY: SensorEntityDescription(
|
||||
# These descriptions should include device class.
|
||||
ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[
|
||||
tuple[str, str], SensorEntityDescription
|
||||
] = {
|
||||
(ENTITY_DESC_KEY_BATTERY, PERCENTAGE): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_BATTERY,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
ENTITY_DESC_KEY_CURRENT: SensorEntityDescription(
|
||||
(ENTITY_DESC_KEY_CURRENT, UnitOfElectricCurrent.AMPERE): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_CURRENT,
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
),
|
||||
ENTITY_DESC_KEY_VOLTAGE: SensorEntityDescription(
|
||||
(ENTITY_DESC_KEY_VOLTAGE, UnitOfElectricPotential.VOLT): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_VOLTAGE,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
|
||||
),
|
||||
ENTITY_DESC_KEY_ENERGY_MEASUREMENT: SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_ENERGY_MEASUREMENT,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
(
|
||||
ENTITY_DESC_KEY_VOLTAGE,
|
||||
UnitOfElectricPotential.MILLIVOLT,
|
||||
): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_VOLTAGE,
|
||||
device_class=SensorDeviceClass.VOLTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
|
||||
),
|
||||
ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING: SensorEntityDescription(
|
||||
(
|
||||
ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING,
|
||||
UnitOfEnergy.KILO_WATT_HOUR,
|
||||
): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING,
|
||||
device_class=SensorDeviceClass.ENERGY,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
|
||||
),
|
||||
ENTITY_DESC_KEY_POWER: SensorEntityDescription(
|
||||
(ENTITY_DESC_KEY_POWER, UnitOfPower.WATT): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_POWER,
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
),
|
||||
ENTITY_DESC_KEY_POWER_FACTOR: SensorEntityDescription(
|
||||
(ENTITY_DESC_KEY_POWER_FACTOR, PERCENTAGE): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_POWER_FACTOR,
|
||||
device_class=SensorDeviceClass.POWER_FACTOR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
ENTITY_DESC_KEY_CO: SensorEntityDescription(
|
||||
(ENTITY_DESC_KEY_CO, CONCENTRATION_PARTS_PER_MILLION): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_CO,
|
||||
device_class=SensorDeviceClass.CO,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
),
|
||||
ENTITY_DESC_KEY_CO2: SensorEntityDescription(
|
||||
(ENTITY_DESC_KEY_CO2, CONCENTRATION_PARTS_PER_MILLION): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_CO2,
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
),
|
||||
ENTITY_DESC_KEY_HUMIDITY: SensorEntityDescription(
|
||||
(ENTITY_DESC_KEY_HUMIDITY, PERCENTAGE): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_HUMIDITY,
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
ENTITY_DESC_KEY_ILLUMINANCE: SensorEntityDescription(
|
||||
(ENTITY_DESC_KEY_ILLUMINANCE, LIGHT_LUX): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_ILLUMINANCE,
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
),
|
||||
ENTITY_DESC_KEY_PRESSURE: SensorEntityDescription(
|
||||
(ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.KPA): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_PRESSURE,
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPressure.KPA,
|
||||
),
|
||||
ENTITY_DESC_KEY_SIGNAL_STRENGTH: SensorEntityDescription(
|
||||
(ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.PSI): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_PRESSURE,
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPressure.PSI,
|
||||
),
|
||||
(ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.INHG): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_PRESSURE,
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPressure.INHG,
|
||||
),
|
||||
(ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.MMHG): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_PRESSURE,
|
||||
device_class=SensorDeviceClass.PRESSURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPressure.MMHG,
|
||||
),
|
||||
(
|
||||
ENTITY_DESC_KEY_SIGNAL_STRENGTH,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_SIGNAL_STRENGTH,
|
||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
),
|
||||
ENTITY_DESC_KEY_TEMPERATURE: SensorEntityDescription(
|
||||
(ENTITY_DESC_KEY_TEMPERATURE, UnitOfTemperature.CELSIUS): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_TEMPERATURE,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
),
|
||||
ENTITY_DESC_KEY_TARGET_TEMPERATURE: SensorEntityDescription(
|
||||
(
|
||||
ENTITY_DESC_KEY_TEMPERATURE,
|
||||
UnitOfTemperature.FAHRENHEIT,
|
||||
): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_TEMPERATURE,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
),
|
||||
(
|
||||
ENTITY_DESC_KEY_TARGET_TEMPERATURE,
|
||||
UnitOfTemperature.CELSIUS,
|
||||
): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_TARGET_TEMPERATURE,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=None,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
),
|
||||
(
|
||||
ENTITY_DESC_KEY_TARGET_TEMPERATURE,
|
||||
UnitOfTemperature.FAHRENHEIT,
|
||||
): SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_TARGET_TEMPERATURE,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
),
|
||||
}
|
||||
|
||||
# These descriptions are without device class.
|
||||
ENTITY_DESCRIPTION_KEY_MAP = {
|
||||
ENTITY_DESC_KEY_CO: SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_CO,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
ENTITY_DESC_KEY_ENERGY_MEASUREMENT: SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_ENERGY_MEASUREMENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
ENTITY_DESC_KEY_HUMIDITY: SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
ENTITY_DESC_KEY_ILLUMINANCE: SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_ILLUMINANCE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
ENTITY_DESC_KEY_POWER_FACTOR: SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_POWER_FACTOR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
ENTITY_DESC_KEY_SIGNAL_STRENGTH: SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_SIGNAL_STRENGTH,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
ENTITY_DESC_KEY_MEASUREMENT: SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_MEASUREMENT,
|
||||
device_class=None,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
ENTITY_DESC_KEY_TOTAL_INCREASING: SensorEntityDescription(
|
||||
ENTITY_DESC_KEY_TOTAL_INCREASING,
|
||||
device_class=None,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def get_entity_description(
|
||||
data: NumericSensorDataTemplateData,
|
||||
) -> SensorEntityDescription:
|
||||
"""Return the entity description for the given data."""
|
||||
data_description_key = data.entity_description_key or ""
|
||||
data_unit = data.unit_of_measurement or ""
|
||||
return ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP.get(
|
||||
(data_description_key, data_unit),
|
||||
ENTITY_DESCRIPTION_KEY_MAP.get(
|
||||
data_description_key,
|
||||
SensorEntityDescription(
|
||||
"base_sensor", native_unit_of_measurement=data.unit_of_measurement
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ConfigEntry,
|
||||
@@ -187,9 +308,8 @@ async def async_setup_entry(
|
||||
data: NumericSensorDataTemplateData = info.platform_data
|
||||
else:
|
||||
data = NumericSensorDataTemplateData()
|
||||
entity_description = ENTITY_DESCRIPTION_KEY_MAP.get(
|
||||
data.entity_description_key or "", SensorEntityDescription("base_sensor")
|
||||
)
|
||||
|
||||
entity_description = get_entity_description(data)
|
||||
|
||||
if info.platform_hint == "string_sensor":
|
||||
entities.append(
|
||||
@@ -308,11 +428,9 @@ class ZWaveNumericSensor(ZwaveSensorBase):
|
||||
@callback
|
||||
def on_value_update(self) -> None:
|
||||
"""Handle scale changes for this value on value updated event."""
|
||||
self._attr_native_unit_of_measurement = (
|
||||
NumericSensorDataTemplate()
|
||||
.resolve_data(self.info.primary_value)
|
||||
.unit_of_measurement
|
||||
)
|
||||
data = NumericSensorDataTemplate().resolve_data(self.info.primary_value)
|
||||
self.entity_description = get_entity_description(data)
|
||||
self._attr_native_unit_of_measurement = data.unit_of_measurement
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
@@ -324,6 +442,8 @@ class ZWaveNumericSensor(ZwaveSensorBase):
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return unit of measurement the value is expressed in."""
|
||||
if self.entity_description.native_unit_of_measurement is not None:
|
||||
return self.entity_description.native_unit_of_measurement
|
||||
if self._attr_native_unit_of_measurement is not None:
|
||||
return self._attr_native_unit_of_measurement
|
||||
if self.info.primary_value.metadata.unit is None:
|
||||
|
||||
@@ -8,7 +8,7 @@ from .backports.enum import StrEnum
|
||||
APPLICATION_NAME: Final = "HomeAssistant"
|
||||
MAJOR_VERSION: Final = 2023
|
||||
MINOR_VERSION: Final = 1
|
||||
PATCH_VERSION: Final = "0b5"
|
||||
PATCH_VERSION: Final = "4"
|
||||
__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}"
|
||||
__version__: Final = f"{__short_version__}.{PATCH_VERSION}"
|
||||
REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0)
|
||||
|
||||
@@ -21,8 +21,8 @@ cryptography==38.0.3
|
||||
dbus-fast==1.82.0
|
||||
fnvhash==0.1.0
|
||||
hass-nabucasa==0.61.0
|
||||
home-assistant-bluetooth==1.9.1
|
||||
home-assistant-frontend==20230102.0
|
||||
home-assistant-bluetooth==1.9.2
|
||||
home-assistant-frontend==20230110.0
|
||||
httpx==0.23.2
|
||||
ifaddr==0.1.7
|
||||
janus==1.0.0
|
||||
|
||||
+2
-2
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "homeassistant"
|
||||
version = "2023.1.0b5"
|
||||
version = "2023.1.4"
|
||||
license = {text = "Apache-2.0"}
|
||||
description = "Open-source home automation platform running on Python 3."
|
||||
readme = "README.rst"
|
||||
@@ -36,7 +36,7 @@ dependencies = [
|
||||
# When bumping httpx, please check the version pins of
|
||||
# httpcore, anyio, and h11 in gen_requirements_all
|
||||
"httpx==0.23.2",
|
||||
"home-assistant-bluetooth==1.9.1",
|
||||
"home-assistant-bluetooth==1.9.2",
|
||||
"ifaddr==0.1.7",
|
||||
"jinja2==3.1.2",
|
||||
"lru-dict==1.1.8",
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ bcrypt==3.1.7
|
||||
certifi>=2021.5.30
|
||||
ciso8601==2.3.0
|
||||
httpx==0.23.2
|
||||
home-assistant-bluetooth==1.9.1
|
||||
home-assistant-bluetooth==1.9.2
|
||||
ifaddr==0.1.7
|
||||
jinja2==3.1.2
|
||||
lru-dict==1.1.8
|
||||
|
||||
+24
-24
@@ -40,7 +40,7 @@ PyRMVtransport==0.3.3
|
||||
PySocks==1.7.1
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.36.1
|
||||
PySwitchbot==0.36.3
|
||||
|
||||
# homeassistant.components.transport_nsw
|
||||
PyTransportNSW==0.1.1
|
||||
@@ -86,7 +86,7 @@ adb-shell[async]==0.4.3
|
||||
adext==0.4.2
|
||||
|
||||
# homeassistant.components.adguard
|
||||
adguardhome==0.5.1
|
||||
adguardhome==0.6.1
|
||||
|
||||
# homeassistant.components.advantage_air
|
||||
advantage_air==0.4.1
|
||||
@@ -220,7 +220,7 @@ aiomodernforms==0.1.8
|
||||
aiomusiccast==0.14.4
|
||||
|
||||
# homeassistant.components.nanoleaf
|
||||
aionanoleaf==0.2.0
|
||||
aionanoleaf==0.2.1
|
||||
|
||||
# homeassistant.components.keyboard_remote
|
||||
aionotify==0.2.0
|
||||
@@ -267,7 +267,7 @@ aiosenseme==0.6.1
|
||||
aiosenz==1.0.0
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==5.2.0
|
||||
aioshelly==5.2.1
|
||||
|
||||
# homeassistant.components.skybell
|
||||
aioskybell==22.7.0
|
||||
@@ -297,7 +297,7 @@ aiovlc==0.1.0
|
||||
aiowatttime==0.1.1
|
||||
|
||||
# homeassistant.components.webostv
|
||||
aiowebostv==0.2.1
|
||||
aiowebostv==0.3.0
|
||||
|
||||
# homeassistant.components.yandex_transport
|
||||
aioymaps==1.2.2
|
||||
@@ -419,10 +419,10 @@ beautifulsoup4==4.11.1
|
||||
# beewi_smartclim==0.0.10
|
||||
|
||||
# homeassistant.components.zha
|
||||
bellows==0.34.5
|
||||
bellows==0.34.6
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer_connected==0.10.4
|
||||
bimmer_connected==0.12.0
|
||||
|
||||
# homeassistant.components.bizkaibus
|
||||
bizkaibus==0.1.1
|
||||
@@ -488,7 +488,7 @@ brunt==1.2.0
|
||||
bt_proximity==0.2.1
|
||||
|
||||
# homeassistant.components.bthome
|
||||
bthome-ble==2.4.0
|
||||
bthome-ble==2.4.1
|
||||
|
||||
# homeassistant.components.bt_home_hub_5
|
||||
bthomehub5-devicelist==0.1.1
|
||||
@@ -744,7 +744,7 @@ gTTS==2.2.4
|
||||
gassist-text==0.0.7
|
||||
|
||||
# homeassistant.components.google
|
||||
gcal-sync==4.1.0
|
||||
gcal-sync==4.1.2
|
||||
|
||||
# homeassistant.components.geniushub
|
||||
geniushub-client==0.6.30
|
||||
@@ -858,7 +858,7 @@ hass-nabucasa==0.61.0
|
||||
hass_splunk==0.1.1
|
||||
|
||||
# homeassistant.components.tasmota
|
||||
hatasmota==0.6.1
|
||||
hatasmota==0.6.3
|
||||
|
||||
# homeassistant.components.jewish_calendar
|
||||
hdate==0.10.4
|
||||
@@ -888,7 +888,7 @@ hole==0.8.0
|
||||
holidays==0.17.2
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20230102.0
|
||||
home-assistant-frontend==20230110.0
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.2
|
||||
@@ -906,7 +906,7 @@ horimote==0.4.1
|
||||
httplib2==0.20.4
|
||||
|
||||
# homeassistant.components.huawei_lte
|
||||
huawei-lte-api==1.6.7
|
||||
huawei-lte-api==1.6.11
|
||||
|
||||
# homeassistant.components.hydrawise
|
||||
hydrawiser==0.2
|
||||
@@ -930,7 +930,7 @@ ibm-watson==5.2.2
|
||||
ibmiotf==0.3.4
|
||||
|
||||
# homeassistant.components.local_calendar
|
||||
ical==4.2.8
|
||||
ical==4.2.9
|
||||
|
||||
# homeassistant.components.ping
|
||||
icmplib==3.0
|
||||
@@ -1029,7 +1029,7 @@ librouteros==3.2.0
|
||||
libsoundtouch==0.8
|
||||
|
||||
# homeassistant.components.life360
|
||||
life360==5.3.0
|
||||
life360==5.5.0
|
||||
|
||||
# homeassistant.components.osramlightify
|
||||
lightify==1.0.7.3
|
||||
@@ -1439,7 +1439,7 @@ pyRFXtrx==0.30.0
|
||||
pySwitchmate==0.5.1
|
||||
|
||||
# homeassistant.components.tibber
|
||||
pyTibber==0.26.7
|
||||
pyTibber==0.26.8
|
||||
|
||||
# homeassistant.components.dlink
|
||||
pyW215==0.7.0
|
||||
@@ -1560,7 +1560,7 @@ pydroid-ipcam==2.0.0
|
||||
pyebox==1.1.4
|
||||
|
||||
# homeassistant.components.econet
|
||||
pyeconet==0.1.17
|
||||
pyeconet==0.1.18
|
||||
|
||||
# homeassistant.components.edimax
|
||||
pyedimax==0.2.1
|
||||
@@ -1719,7 +1719,7 @@ pylibrespot-java==0.1.1
|
||||
pylitejet==0.3.0
|
||||
|
||||
# homeassistant.components.litterrobot
|
||||
pylitterbot==2022.12.0
|
||||
pylitterbot==2023.1.1
|
||||
|
||||
# homeassistant.components.lutron_caseta
|
||||
pylutron-caseta==0.17.1
|
||||
@@ -1803,7 +1803,7 @@ pyoctoprintapi==0.1.9
|
||||
pyombi==0.1.10
|
||||
|
||||
# homeassistant.components.openuv
|
||||
pyopenuv==2022.04.0
|
||||
pyopenuv==2023.01.0
|
||||
|
||||
# homeassistant.components.opnsense
|
||||
pyopnsense==0.2.0
|
||||
@@ -1832,7 +1832,7 @@ pyownet==0.10.0.post1
|
||||
pypca==0.0.7
|
||||
|
||||
# homeassistant.components.lcn
|
||||
pypck==0.7.15
|
||||
pypck==0.7.16
|
||||
|
||||
# homeassistant.components.pjlink
|
||||
pypjlink2==1.2.1
|
||||
@@ -2109,7 +2109,7 @@ pytrafikverket==0.2.2
|
||||
pyudev==0.23.2
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
pyunifiprotect==4.5.2
|
||||
pyunifiprotect==4.6.1
|
||||
|
||||
# homeassistant.components.uptimerobot
|
||||
pyuptimerobot==22.2.0
|
||||
@@ -2190,7 +2190,7 @@ regenmaschine==2022.11.0
|
||||
renault-api==0.1.11
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.1.1
|
||||
reolink-aio==0.1.3
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==5.2
|
||||
@@ -2269,7 +2269,7 @@ sendgrid==6.8.2
|
||||
|
||||
# homeassistant.components.emulated_kasa
|
||||
# homeassistant.components.sense
|
||||
sense_energy==0.11.0
|
||||
sense_energy==0.11.1
|
||||
|
||||
# homeassistant.components.sensirion_ble
|
||||
sensirion-ble==0.0.1
|
||||
@@ -2668,13 +2668,13 @@ zigpy-zigate==0.10.3
|
||||
zigpy-znp==0.9.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.52.3
|
||||
zigpy==0.53.0
|
||||
|
||||
# homeassistant.components.zoneminder
|
||||
zm-py==0.5.2
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.43.1
|
||||
zwave-js-server-python==0.44.0
|
||||
|
||||
# homeassistant.components.zwave_me
|
||||
zwave_me_ws==0.3.0
|
||||
|
||||
+24
-24
@@ -36,7 +36,7 @@ PyRMVtransport==0.3.3
|
||||
PySocks==1.7.1
|
||||
|
||||
# homeassistant.components.switchbot
|
||||
PySwitchbot==0.36.1
|
||||
PySwitchbot==0.36.3
|
||||
|
||||
# homeassistant.components.transport_nsw
|
||||
PyTransportNSW==0.1.1
|
||||
@@ -76,7 +76,7 @@ adb-shell[async]==0.4.3
|
||||
adext==0.4.2
|
||||
|
||||
# homeassistant.components.adguard
|
||||
adguardhome==0.5.1
|
||||
adguardhome==0.6.1
|
||||
|
||||
# homeassistant.components.advantage_air
|
||||
advantage_air==0.4.1
|
||||
@@ -198,7 +198,7 @@ aiomodernforms==0.1.8
|
||||
aiomusiccast==0.14.4
|
||||
|
||||
# homeassistant.components.nanoleaf
|
||||
aionanoleaf==0.2.0
|
||||
aionanoleaf==0.2.1
|
||||
|
||||
# homeassistant.components.notion
|
||||
aionotion==3.0.2
|
||||
@@ -242,7 +242,7 @@ aiosenseme==0.6.1
|
||||
aiosenz==1.0.0
|
||||
|
||||
# homeassistant.components.shelly
|
||||
aioshelly==5.2.0
|
||||
aioshelly==5.2.1
|
||||
|
||||
# homeassistant.components.skybell
|
||||
aioskybell==22.7.0
|
||||
@@ -272,7 +272,7 @@ aiovlc==0.1.0
|
||||
aiowatttime==0.1.1
|
||||
|
||||
# homeassistant.components.webostv
|
||||
aiowebostv==0.2.1
|
||||
aiowebostv==0.3.0
|
||||
|
||||
# homeassistant.components.yandex_transport
|
||||
aioymaps==1.2.2
|
||||
@@ -346,10 +346,10 @@ base36==0.1.1
|
||||
beautifulsoup4==4.11.1
|
||||
|
||||
# homeassistant.components.zha
|
||||
bellows==0.34.5
|
||||
bellows==0.34.6
|
||||
|
||||
# homeassistant.components.bmw_connected_drive
|
||||
bimmer_connected==0.10.4
|
||||
bimmer_connected==0.12.0
|
||||
|
||||
# homeassistant.components.bluetooth
|
||||
bleak-retry-connector==2.13.0
|
||||
@@ -392,7 +392,7 @@ brother==2.1.1
|
||||
brunt==1.2.0
|
||||
|
||||
# homeassistant.components.bthome
|
||||
bthome-ble==2.4.0
|
||||
bthome-ble==2.4.1
|
||||
|
||||
# homeassistant.components.buienradar
|
||||
buienradar==1.0.5
|
||||
@@ -560,7 +560,7 @@ gTTS==2.2.4
|
||||
gassist-text==0.0.7
|
||||
|
||||
# homeassistant.components.google
|
||||
gcal-sync==4.1.0
|
||||
gcal-sync==4.1.2
|
||||
|
||||
# homeassistant.components.geocaching
|
||||
geocachingapi==0.2.1
|
||||
@@ -647,7 +647,7 @@ habitipy==0.2.0
|
||||
hass-nabucasa==0.61.0
|
||||
|
||||
# homeassistant.components.tasmota
|
||||
hatasmota==0.6.1
|
||||
hatasmota==0.6.3
|
||||
|
||||
# homeassistant.components.jewish_calendar
|
||||
hdate==0.10.4
|
||||
@@ -668,7 +668,7 @@ hole==0.8.0
|
||||
holidays==0.17.2
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20230102.0
|
||||
home-assistant-frontend==20230110.0
|
||||
|
||||
# homeassistant.components.home_connect
|
||||
homeconnect==0.7.2
|
||||
@@ -683,7 +683,7 @@ homepluscontrol==0.0.5
|
||||
httplib2==0.20.4
|
||||
|
||||
# homeassistant.components.huawei_lte
|
||||
huawei-lte-api==1.6.7
|
||||
huawei-lte-api==1.6.11
|
||||
|
||||
# homeassistant.components.hyperion
|
||||
hyperion-py==0.7.5
|
||||
@@ -695,7 +695,7 @@ iaqualink==0.5.0
|
||||
ibeacon_ble==1.0.1
|
||||
|
||||
# homeassistant.components.local_calendar
|
||||
ical==4.2.8
|
||||
ical==4.2.9
|
||||
|
||||
# homeassistant.components.ping
|
||||
icmplib==3.0
|
||||
@@ -767,7 +767,7 @@ librouteros==3.2.0
|
||||
libsoundtouch==0.8
|
||||
|
||||
# homeassistant.components.life360
|
||||
life360==5.3.0
|
||||
life360==5.5.0
|
||||
|
||||
# homeassistant.components.logi_circle
|
||||
logi_circle==0.2.3
|
||||
@@ -1039,7 +1039,7 @@ pyMetno==0.9.0
|
||||
pyRFXtrx==0.30.0
|
||||
|
||||
# homeassistant.components.tibber
|
||||
pyTibber==0.26.7
|
||||
pyTibber==0.26.8
|
||||
|
||||
# homeassistant.components.nextbus
|
||||
py_nextbusnext==0.1.5
|
||||
@@ -1106,7 +1106,7 @@ pydexcom==0.2.3
|
||||
pydroid-ipcam==2.0.0
|
||||
|
||||
# homeassistant.components.econet
|
||||
pyeconet==0.1.17
|
||||
pyeconet==0.1.18
|
||||
|
||||
# homeassistant.components.efergy
|
||||
pyefergy==22.1.1
|
||||
@@ -1220,7 +1220,7 @@ pylibrespot-java==0.1.1
|
||||
pylitejet==0.3.0
|
||||
|
||||
# homeassistant.components.litterrobot
|
||||
pylitterbot==2022.12.0
|
||||
pylitterbot==2023.1.1
|
||||
|
||||
# homeassistant.components.lutron_caseta
|
||||
pylutron-caseta==0.17.1
|
||||
@@ -1283,7 +1283,7 @@ pynzbgetapi==0.2.0
|
||||
pyoctoprintapi==0.1.9
|
||||
|
||||
# homeassistant.components.openuv
|
||||
pyopenuv==2022.04.0
|
||||
pyopenuv==2023.01.0
|
||||
|
||||
# homeassistant.components.opnsense
|
||||
pyopnsense==0.2.0
|
||||
@@ -1306,7 +1306,7 @@ pyowm==3.2.0
|
||||
pyownet==0.10.0.post1
|
||||
|
||||
# homeassistant.components.lcn
|
||||
pypck==0.7.15
|
||||
pypck==0.7.16
|
||||
|
||||
# homeassistant.components.plaato
|
||||
pyplaato==0.0.18
|
||||
@@ -1475,7 +1475,7 @@ pytrafikverket==0.2.2
|
||||
pyudev==0.23.2
|
||||
|
||||
# homeassistant.components.unifiprotect
|
||||
pyunifiprotect==4.5.2
|
||||
pyunifiprotect==4.6.1
|
||||
|
||||
# homeassistant.components.uptimerobot
|
||||
pyuptimerobot==22.2.0
|
||||
@@ -1529,7 +1529,7 @@ regenmaschine==2022.11.0
|
||||
renault-api==0.1.11
|
||||
|
||||
# homeassistant.components.reolink
|
||||
reolink-aio==0.1.1
|
||||
reolink-aio==0.1.3
|
||||
|
||||
# homeassistant.components.python_script
|
||||
restrictedpython==5.2
|
||||
@@ -1578,7 +1578,7 @@ securetar==2022.2.0
|
||||
|
||||
# homeassistant.components.emulated_kasa
|
||||
# homeassistant.components.sense
|
||||
sense_energy==0.11.0
|
||||
sense_energy==0.11.1
|
||||
|
||||
# homeassistant.components.sensirion_ble
|
||||
sensirion-ble==0.0.1
|
||||
@@ -1869,10 +1869,10 @@ zigpy-zigate==0.10.3
|
||||
zigpy-znp==0.9.2
|
||||
|
||||
# homeassistant.components.zha
|
||||
zigpy==0.52.3
|
||||
zigpy==0.53.0
|
||||
|
||||
# homeassistant.components.zwave_js
|
||||
zwave-js-server-python==0.43.1
|
||||
zwave-js-server-python==0.44.0
|
||||
|
||||
# homeassistant.components.zwave_me
|
||||
zwave_me_ws==0.3.0
|
||||
|
||||
@@ -4,7 +4,6 @@ import json
|
||||
from pathlib import Path
|
||||
|
||||
from bimmer_connected.account import MyBMWAccount
|
||||
from bimmer_connected.api.utils import log_to_to_file
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.bmw_connected_drive.const import (
|
||||
@@ -64,15 +63,6 @@ async def mock_vehicles_from_fixture(account: MyBMWAccount) -> None:
|
||||
}
|
||||
fetched_at = utcnow()
|
||||
|
||||
# simulate storing fingerprints
|
||||
if account.config.log_response_path:
|
||||
for brand in ["bmw", "mini"]:
|
||||
log_to_to_file(
|
||||
json.dumps(vehicles[brand]),
|
||||
account.config.log_response_path,
|
||||
f"vehicles_v2_{brand}",
|
||||
)
|
||||
|
||||
# Create a vehicle with base + specific state as provided by state/VIN API
|
||||
for vehicle_base in [vehicle for brand in vehicles.values() for vehicle in brand]:
|
||||
vehicle_state_path = (
|
||||
@@ -93,14 +83,6 @@ async def mock_vehicles_from_fixture(account: MyBMWAccount) -> None:
|
||||
fetched_at,
|
||||
)
|
||||
|
||||
# simulate storing fingerprints
|
||||
if account.config.log_response_path:
|
||||
log_to_to_file(
|
||||
json.dumps(vehicle_state),
|
||||
account.config.log_response_path,
|
||||
f"state_{vehicle_base['vin']}",
|
||||
)
|
||||
|
||||
|
||||
async def setup_mocked_integration(hass: HomeAssistant) -> MockConfigEntry:
|
||||
"""Mock a fully setup config entry and all components based on fixtures."""
|
||||
|
||||
@@ -4,6 +4,7 @@ from unittest.mock import patch
|
||||
from pycfdns.exceptions import (
|
||||
CloudflareAuthenticationException,
|
||||
CloudflareConnectionException,
|
||||
CloudflareZoneException,
|
||||
)
|
||||
import pytest
|
||||
|
||||
@@ -31,14 +32,21 @@ async def test_unload_entry(hass, cfupdate):
|
||||
assert not hass.data.get(DOMAIN)
|
||||
|
||||
|
||||
async def test_async_setup_raises_entry_not_ready(hass, cfupdate):
|
||||
@pytest.mark.parametrize(
|
||||
"side_effect",
|
||||
(
|
||||
CloudflareConnectionException(),
|
||||
CloudflareZoneException(),
|
||||
),
|
||||
)
|
||||
async def test_async_setup_raises_entry_not_ready(hass, cfupdate, side_effect):
|
||||
"""Test that it throws ConfigEntryNotReady when exception occurs during setup."""
|
||||
instance = cfupdate.return_value
|
||||
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
instance.get_zone_id.side_effect = CloudflareConnectionException()
|
||||
instance.get_zone_id.side_effect = side_effect
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
|
||||
assert entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
@@ -26,6 +26,7 @@ from homeassistant.const import (
|
||||
STATE_UNAVAILABLE,
|
||||
STATE_UNKNOWN,
|
||||
VOLUME_CUBIC_METERS,
|
||||
UnitOfEnergy,
|
||||
UnitOfPower,
|
||||
)
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -804,3 +805,57 @@ async def test_reconnect(hass, dsmr_connection_fixture):
|
||||
await hass.config_entries.async_unload(mock_entry.entry_id)
|
||||
|
||||
assert mock_entry.state == config_entries.ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_gas_meter_providing_energy_reading(hass, dsmr_connection_fixture):
|
||||
"""Test that gas providing energy readings use the correct device class."""
|
||||
(connection_factory, transport, protocol) = dsmr_connection_fixture
|
||||
|
||||
from dsmr_parser.obis_references import GAS_METER_READING
|
||||
from dsmr_parser.objects import MBusObject
|
||||
|
||||
entry_data = {
|
||||
"port": "/dev/ttyUSB0",
|
||||
"dsmr_version": "2.2",
|
||||
"precision": 4,
|
||||
"reconnect_interval": 30,
|
||||
"serial_id": "1234",
|
||||
"serial_id_gas": "5678",
|
||||
}
|
||||
entry_options = {
|
||||
"time_between_update": 0,
|
||||
}
|
||||
|
||||
telegram = {
|
||||
GAS_METER_READING: MBusObject(
|
||||
[
|
||||
{"value": datetime.datetime.fromtimestamp(1551642213)},
|
||||
{"value": Decimal(123.456), "unit": UnitOfEnergy.GIGA_JOULE},
|
||||
]
|
||||
),
|
||||
}
|
||||
|
||||
mock_entry = MockConfigEntry(
|
||||
domain="dsmr", unique_id="/dev/ttyUSB0", data=entry_data, options=entry_options
|
||||
)
|
||||
|
||||
mock_entry.add_to_hass(hass)
|
||||
|
||||
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]
|
||||
telegram_callback(telegram)
|
||||
await asyncio.sleep(0)
|
||||
|
||||
gas_consumption = hass.states.get("sensor.gas_meter_gas_consumption")
|
||||
assert gas_consumption.state == "123.456"
|
||||
assert gas_consumption.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY
|
||||
assert (
|
||||
gas_consumption.attributes.get(ATTR_STATE_CLASS)
|
||||
== SensorStateClass.TOTAL_INCREASING
|
||||
)
|
||||
assert (
|
||||
gas_consumption.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
== UnitOfEnergy.GIGA_JOULE
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ from collections.abc import Generator
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from gspread import GSpreadException
|
||||
import oauth2client
|
||||
import pytest
|
||||
|
||||
from homeassistant import config_entries
|
||||
@@ -21,6 +20,8 @@ from tests.common import MockConfigEntry
|
||||
|
||||
CLIENT_ID = "1234"
|
||||
CLIENT_SECRET = "5678"
|
||||
GOOGLE_AUTH_URI = "https://accounts.google.com/o/oauth2/v2/auth"
|
||||
GOOGLE_TOKEN_URI = "https://oauth2.googleapis.com/token"
|
||||
SHEET_ID = "google-sheet-id"
|
||||
TITLE = "Google Sheets"
|
||||
|
||||
@@ -66,7 +67,7 @@ async def test_full_flow(
|
||||
)
|
||||
|
||||
assert result["url"] == (
|
||||
f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}"
|
||||
f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}"
|
||||
"&redirect_uri=https://example.com/auth/external/callback"
|
||||
f"&state={state}&scope=https://www.googleapis.com/auth/drive.file"
|
||||
"&access_type=offline&prompt=consent"
|
||||
@@ -83,7 +84,7 @@ async def test_full_flow(
|
||||
mock_client.return_value.create = mock_create
|
||||
|
||||
aioclient_mock.post(
|
||||
oauth2client.GOOGLE_TOKEN_URI,
|
||||
GOOGLE_TOKEN_URI,
|
||||
json={
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"access_token": "mock-access-token",
|
||||
@@ -133,7 +134,7 @@ async def test_create_sheet_error(
|
||||
)
|
||||
|
||||
assert result["url"] == (
|
||||
f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}"
|
||||
f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}"
|
||||
"&redirect_uri=https://example.com/auth/external/callback"
|
||||
f"&state={state}&scope=https://www.googleapis.com/auth/drive.file"
|
||||
"&access_type=offline&prompt=consent"
|
||||
@@ -150,7 +151,7 @@ async def test_create_sheet_error(
|
||||
mock_client.return_value.create = mock_create
|
||||
|
||||
aioclient_mock.post(
|
||||
oauth2client.GOOGLE_TOKEN_URI,
|
||||
GOOGLE_TOKEN_URI,
|
||||
json={
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"access_token": "mock-access-token",
|
||||
@@ -202,7 +203,7 @@ async def test_reauth(
|
||||
},
|
||||
)
|
||||
assert result["url"] == (
|
||||
f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}"
|
||||
f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}"
|
||||
"&redirect_uri=https://example.com/auth/external/callback"
|
||||
f"&state={state}&scope=https://www.googleapis.com/auth/drive.file"
|
||||
"&access_type=offline&prompt=consent"
|
||||
@@ -218,7 +219,7 @@ async def test_reauth(
|
||||
mock_client.return_value.open_by_key = mock_open
|
||||
|
||||
aioclient_mock.post(
|
||||
oauth2client.GOOGLE_TOKEN_URI,
|
||||
GOOGLE_TOKEN_URI,
|
||||
json={
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"access_token": "updated-access-token",
|
||||
@@ -283,7 +284,7 @@ async def test_reauth_abort(
|
||||
},
|
||||
)
|
||||
assert result["url"] == (
|
||||
f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}"
|
||||
f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}"
|
||||
"&redirect_uri=https://example.com/auth/external/callback"
|
||||
f"&state={state}&scope=https://www.googleapis.com/auth/drive.file"
|
||||
"&access_type=offline&prompt=consent"
|
||||
@@ -300,7 +301,7 @@ async def test_reauth_abort(
|
||||
mock_client.return_value.open_by_key = mock_open
|
||||
|
||||
aioclient_mock.post(
|
||||
oauth2client.GOOGLE_TOKEN_URI,
|
||||
GOOGLE_TOKEN_URI,
|
||||
json={
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"access_token": "updated-access-token",
|
||||
@@ -346,7 +347,7 @@ async def test_already_configured(
|
||||
)
|
||||
|
||||
assert result["url"] == (
|
||||
f"{oauth2client.GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}"
|
||||
f"{GOOGLE_AUTH_URI}?response_type=code&client_id={CLIENT_ID}"
|
||||
"&redirect_uri=https://example.com/auth/external/callback"
|
||||
f"&state={state}&scope=https://www.googleapis.com/auth/drive.file"
|
||||
"&access_type=offline&prompt=consent"
|
||||
@@ -363,7 +364,7 @@ async def test_already_configured(
|
||||
mock_client.return_value.create = mock_create
|
||||
|
||||
aioclient_mock.post(
|
||||
oauth2client.GOOGLE_TOKEN_URI,
|
||||
GOOGLE_TOKEN_URI,
|
||||
json={
|
||||
"refresh_token": "mock-refresh-token",
|
||||
"access_token": "mock-access-token",
|
||||
|
||||
@@ -211,9 +211,14 @@ async def test_success(hass, login_requests_mock):
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("upnp_data", "expected_result"),
|
||||
("requests_mock_request_kwargs", "upnp_data", "expected_result"),
|
||||
(
|
||||
(
|
||||
{
|
||||
"method": ANY,
|
||||
"url": f"{FIXTURE_USER_INPUT[CONF_URL]}api/device/basic_information",
|
||||
"text": "<response><devicename>Mock device</devicename></response>",
|
||||
},
|
||||
{
|
||||
ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi",
|
||||
ssdp.ATTR_UPNP_SERIAL: "00000000",
|
||||
@@ -225,6 +230,11 @@ async def test_success(hass, login_requests_mock):
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
"method": ANY,
|
||||
"url": f"{FIXTURE_USER_INPUT[CONF_URL]}api/device/basic_information",
|
||||
"text": "<error><code>100002</code><message/></error>",
|
||||
},
|
||||
{
|
||||
ssdp.ATTR_UPNP_FRIENDLY_NAME: "Mobile Wi-Fi",
|
||||
# No ssdp.ATTR_UPNP_SERIAL
|
||||
@@ -235,19 +245,36 @@ async def test_success(hass, login_requests_mock):
|
||||
"errors": {},
|
||||
},
|
||||
),
|
||||
(
|
||||
{
|
||||
"method": ANY,
|
||||
"url": f"{FIXTURE_USER_INPUT[CONF_URL]}api/device/basic_information",
|
||||
"exc": Exception("Something unexpected"),
|
||||
},
|
||||
{
|
||||
# Does not matter
|
||||
},
|
||||
{
|
||||
"type": data_entry_flow.FlowResultType.ABORT,
|
||||
"reason": "unsupported_device",
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
async def test_ssdp(hass, upnp_data, expected_result):
|
||||
async def test_ssdp(
|
||||
hass, login_requests_mock, requests_mock_request_kwargs, upnp_data, expected_result
|
||||
):
|
||||
"""Test SSDP discovery initiates config properly."""
|
||||
url = "http://192.168.100.1/"
|
||||
url = FIXTURE_USER_INPUT[CONF_URL][:-1] # strip trailing slash for appending port
|
||||
context = {"source": config_entries.SOURCE_SSDP}
|
||||
login_requests_mock.request(**requests_mock_request_kwargs)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context=context,
|
||||
data=ssdp.SsdpServiceInfo(
|
||||
ssdp_usn="mock_usn",
|
||||
ssdp_st="upnp:rootdevice",
|
||||
ssdp_location="http://192.168.100.1:60957/rootDesc.xml",
|
||||
ssdp_location=f"{url}:60957/rootDesc.xml",
|
||||
upnp={
|
||||
ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:InternetGatewayDevice:1",
|
||||
ssdp.ATTR_UPNP_MANUFACTURER: "Huawei",
|
||||
@@ -264,7 +291,7 @@ async def test_ssdp(hass, upnp_data, expected_result):
|
||||
for k, v in expected_result.items():
|
||||
assert result[k] == v
|
||||
if result.get("data_schema"):
|
||||
result["data_schema"]({})[CONF_URL] == url
|
||||
assert result["data_schema"]({})[CONF_URL] == url + "/"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
|
||||
@@ -11,7 +11,13 @@ from homeassistant.components.update import (
|
||||
SERVICE_INSTALL,
|
||||
UpdateDeviceClass,
|
||||
)
|
||||
from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, STATE_OFF, STATE_ON
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
ATTR_ENTITY_ID,
|
||||
STATE_OFF,
|
||||
STATE_ON,
|
||||
STATE_UNKNOWN,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
@@ -28,6 +34,7 @@ async def test_robot_with_no_update(
|
||||
"""Tests the update entity was set up."""
|
||||
robot: LitterRobot4 = mock_account_with_litterrobot_4.robots[0]
|
||||
robot.has_firmware_update = AsyncMock(return_value=False)
|
||||
robot.get_latest_firmware = AsyncMock(return_value=None)
|
||||
|
||||
entry = await setup_integration(
|
||||
hass, mock_account_with_litterrobot_4, PLATFORM_DOMAIN
|
||||
@@ -79,3 +86,27 @@ async def test_robot_with_update(
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert robot.update_firmware.call_count == 1
|
||||
|
||||
|
||||
async def test_robot_with_update_already_in_progress(
|
||||
hass: HomeAssistant, mock_account_with_litterrobot_4: MagicMock
|
||||
):
|
||||
"""Tests the update entity was set up."""
|
||||
robot: LitterRobot4 = mock_account_with_litterrobot_4.robots[0]
|
||||
robot._update_data( # pylint:disable=protected-access
|
||||
{"isFirmwareUpdateTriggered": True}, partial=True
|
||||
)
|
||||
|
||||
entry = await setup_integration(
|
||||
hass, mock_account_with_litterrobot_4, PLATFORM_DOMAIN
|
||||
)
|
||||
|
||||
state = hass.states.get(ENTITY_ID)
|
||||
assert state
|
||||
assert state.state == STATE_UNKNOWN
|
||||
assert state.attributes[ATTR_DEVICE_CLASS] == UpdateDeviceClass.FIRMWARE
|
||||
assert state.attributes[ATTR_INSTALLED_VERSION] == OLD_FIRMWARE
|
||||
assert state.attributes[ATTR_LATEST_VERSION] is None
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
@@ -18,6 +18,7 @@ from homeassistant.const import (
|
||||
SERVICE_RELOAD,
|
||||
STATE_UNAVAILABLE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.generated.mqtt import MQTT
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
@@ -1820,3 +1821,15 @@ async def help_test_unload_config_entry_with_platform(
|
||||
|
||||
discovery_setup_entity = hass.states.get(f"{domain}.discovery_setup")
|
||||
assert discovery_setup_entity is None
|
||||
|
||||
|
||||
async def help_test_discovery_setup(
|
||||
hass: HomeAssistant, domain: str, discovery_data_payload: str, name: str
|
||||
) -> None:
|
||||
"""Test setting up an MQTT entity using discovery."""
|
||||
async_fire_mqtt_message(
|
||||
hass, f"homeassistant/{domain}/{name}/config", discovery_data_payload
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
state = hass.states.get(f"{domain}.{name}")
|
||||
assert state.state is not None
|
||||
|
||||
@@ -29,6 +29,7 @@ from .test_common import (
|
||||
help_test_default_availability_payload,
|
||||
help_test_discovery_broken,
|
||||
help_test_discovery_removal,
|
||||
help_test_discovery_setup,
|
||||
help_test_discovery_update,
|
||||
help_test_discovery_update_attr,
|
||||
help_test_discovery_update_unchanged,
|
||||
@@ -455,7 +456,7 @@ async def test_discovery_update_select(hass, mqtt_mock_entry_no_yaml_config, cap
|
||||
"name": "Milk",
|
||||
"state_topic": "test-topic",
|
||||
"command_topic": "test-topic",
|
||||
"options": ["milk", "beer"],
|
||||
"options": ["milk"],
|
||||
}
|
||||
|
||||
await help_test_discovery_update(
|
||||
@@ -701,3 +702,27 @@ async def test_unload_entry(hass, mqtt_mock_entry_with_yaml_config, tmp_path):
|
||||
await help_test_unload_config_entry_with_platform(
|
||||
hass, mqtt_mock_entry_with_yaml_config, tmp_path, domain, config
|
||||
)
|
||||
|
||||
|
||||
async def test_persistent_state_after_reconfig(
|
||||
hass: ha.HomeAssistant, mqtt_mock_entry_no_yaml_config
|
||||
) -> None:
|
||||
"""Test of the state is persistent after reconfiguring the select options."""
|
||||
await mqtt_mock_entry_no_yaml_config()
|
||||
discovery_data = '{ "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["milk", "beer"]}'
|
||||
await help_test_discovery_setup(hass, SELECT_DOMAIN, discovery_data, "milk")
|
||||
|
||||
# assign an initial state
|
||||
async_fire_mqtt_message(hass, "test-topic", "beer")
|
||||
state = hass.states.get("select.milk")
|
||||
assert state.state == "beer"
|
||||
assert state.attributes["options"] == ["milk", "beer"]
|
||||
|
||||
# remove "milk" option
|
||||
discovery_data = '{ "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic", "options": ["beer"]}'
|
||||
await help_test_discovery_setup(hass, SELECT_DOMAIN, discovery_data, "milk")
|
||||
|
||||
# assert the state persistent
|
||||
state = hass.states.get("select.milk")
|
||||
assert state.state == "beer"
|
||||
assert state.attributes["options"] == ["beer"]
|
||||
|
||||
@@ -690,6 +690,11 @@ async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config):
|
||||
"device_class": "temperature",
|
||||
},
|
||||
{"name": "Test 2", "state_topic": "test-topic"},
|
||||
{
|
||||
"name": "Test 3",
|
||||
"state_topic": "test-topic",
|
||||
"device_class": None,
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -701,6 +706,8 @@ async def test_valid_device_class(hass, mqtt_mock_entry_with_yaml_config):
|
||||
assert state.attributes["device_class"] == "temperature"
|
||||
state = hass.states.get("sensor.test_2")
|
||||
assert "device_class" not in state.attributes
|
||||
state = hass.states.get("sensor.test_3")
|
||||
assert "device_class" not in state.attributes
|
||||
|
||||
|
||||
async def test_invalid_state_class(hass, mqtt_mock_entry_no_yaml_config):
|
||||
@@ -739,6 +746,11 @@ async def test_valid_state_class(hass, mqtt_mock_entry_with_yaml_config):
|
||||
"state_class": "measurement",
|
||||
},
|
||||
{"name": "Test 2", "state_topic": "test-topic"},
|
||||
{
|
||||
"name": "Test 3",
|
||||
"state_topic": "test-topic",
|
||||
"state_class": None,
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
@@ -750,6 +762,8 @@ async def test_valid_state_class(hass, mqtt_mock_entry_with_yaml_config):
|
||||
assert state.attributes["state_class"] == "measurement"
|
||||
state = hass.states.get("sensor.test_2")
|
||||
assert "state_class" not in state.attributes
|
||||
state = hass.states.get("sensor.test_3")
|
||||
assert "state_class" not in state.attributes
|
||||
|
||||
|
||||
async def test_setting_attribute_via_mqtt_json_message(
|
||||
|
||||
@@ -205,7 +205,6 @@ async def test_options_add_sensor(
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
"last_update_sensor_add": True,
|
||||
"sensor_indices": [123456, 567890],
|
||||
}
|
||||
|
||||
@@ -265,7 +264,6 @@ async def test_options_remove_sensor(hass, config_entry, setup_purpleair):
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {
|
||||
"last_update_sensor_add": False,
|
||||
"sensor_indices": [],
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Test the Shelly config flow."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import replace
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
from aioshelly.exceptions import (
|
||||
@@ -12,6 +13,7 @@ import pytest
|
||||
|
||||
from homeassistant import config_entries, data_entry_flow
|
||||
from homeassistant.components import zeroconf
|
||||
from homeassistant.components.shelly import config_flow
|
||||
from homeassistant.components.shelly.const import (
|
||||
CONF_BLE_SCANNER_MODE,
|
||||
DOMAIN,
|
||||
@@ -704,6 +706,30 @@ async def test_zeroconf_already_configured(hass):
|
||||
assert entry.data["host"] == "1.1.1.1"
|
||||
|
||||
|
||||
async def test_zeroconf_with_wifi_ap_ip(hass):
|
||||
"""Test we ignore the Wi-FI AP IP."""
|
||||
|
||||
entry = MockConfigEntry(
|
||||
domain="shelly", unique_id="test-mac", data={"host": "2.2.2.2"}
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"aioshelly.common.get_info",
|
||||
return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False},
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
data=replace(DISCOVERY_INFO, host=config_flow.INTERNAL_WIFI_AP_IP),
|
||||
context={"source": config_entries.SOURCE_ZEROCONF},
|
||||
)
|
||||
assert result["type"] == data_entry_flow.FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
# Test config entry was not updated with the wifi ap ip
|
||||
assert entry.data["host"] == "2.2.2.2"
|
||||
|
||||
|
||||
async def test_zeroconf_firmware_unsupported(hass):
|
||||
"""Test we abort if device firmware is unsupported."""
|
||||
with patch("aioshelly.common.get_info", side_effect=FirmwareUnsupported):
|
||||
|
||||
@@ -213,7 +213,7 @@ async def test_entry_unload_not_connected(hass, mock_rpc_device, monkeypatch):
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
async def test_entry_unload_not_connected_but_we_with_we_are(
|
||||
async def test_entry_unload_not_connected_but_we_think_we_are(
|
||||
hass, mock_rpc_device, monkeypatch
|
||||
):
|
||||
"""Test entry unload when not connected but we think we are still connected."""
|
||||
@@ -238,3 +238,17 @@ async def test_entry_unload_not_connected_but_we_with_we_are(
|
||||
|
||||
assert not mock_stop_scanner.call_count
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
async def test_no_attempt_to_stop_scanner_with_sleepy_devices(hass, mock_rpc_device):
|
||||
"""Test we do not try to stop the scanner if its disabled with a sleepy device."""
|
||||
with patch(
|
||||
"homeassistant.components.shelly.coordinator.async_stop_scanner",
|
||||
) as mock_stop_scanner:
|
||||
entry = await init_integration(hass, 2, sleep_period=7200)
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
assert not mock_stop_scanner.call_count
|
||||
|
||||
mock_rpc_device.mock_update()
|
||||
await hass.async_block_till_done()
|
||||
assert not mock_stop_scanner.call_count
|
||||
|
||||
@@ -481,7 +481,7 @@ async def test_user_setup_wolock_auth(hass):
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key",
|
||||
side_effect=SwitchbotAuthenticationError,
|
||||
side_effect=SwitchbotAuthenticationError("error from api"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
@@ -494,6 +494,7 @@ async def test_user_setup_wolock_auth(hass):
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "lock_auth"
|
||||
assert result["errors"] == {"base": "auth_failed"}
|
||||
assert "error from api" in result["description_placeholders"]["error_detail"]
|
||||
|
||||
with patch_async_setup_entry() as mock_setup_entry, patch(
|
||||
"homeassistant.components.switchbot.config_flow.SwitchbotLock.verify_encryption_key",
|
||||
|
||||
@@ -214,6 +214,7 @@ def doorbell_fixture(camera: Camera, fixed_now: datetime):
|
||||
doorbell.feature_flags.has_lcd_screen = True
|
||||
doorbell.feature_flags.has_speaker = True
|
||||
doorbell.feature_flags.has_privacy_mask = True
|
||||
doorbell.feature_flags.is_doorbell = True
|
||||
doorbell.feature_flags.has_chime = True
|
||||
doorbell.feature_flags.has_smart_detect = True
|
||||
doorbell.feature_flags.has_package_camera = True
|
||||
|
||||
@@ -77,7 +77,7 @@ def update_attribute_cache(cluster):
|
||||
attrid = zigpy.types.uint16_t(attrid)
|
||||
attrs.append(make_attribute(attrid, value))
|
||||
|
||||
hdr = make_zcl_header(zcl_f.Command.Report_Attributes)
|
||||
hdr = make_zcl_header(zcl_f.GeneralCommand.Report_Attributes)
|
||||
hdr.frame_control.disable_default_response = True
|
||||
msg = zcl_f.GENERAL_COMMANDS[zcl_f.GeneralCommand.Report_Attributes].schema(
|
||||
attribute_reports=attrs
|
||||
|
||||
@@ -3,12 +3,14 @@ import asyncio
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
import zigpy.exceptions
|
||||
import zigpy.profiles.zha as zha
|
||||
import zigpy.zcl.clusters.general as general
|
||||
import zigpy.zcl.clusters.lighting as lighting
|
||||
|
||||
from homeassistant.components.zha.core.group import GroupMember
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .common import async_find_group_entity_id, get_zha_gateway
|
||||
from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE
|
||||
@@ -259,3 +261,20 @@ async def test_gateway_initialize_failure(hass, device_light_1, coordinator):
|
||||
await zha_gateway.async_initialize()
|
||||
|
||||
assert mock_new.call_count == 3
|
||||
|
||||
|
||||
@patch("homeassistant.components.zha.core.gateway.STARTUP_FAILURE_DELAY_S", 0.01)
|
||||
async def test_gateway_initialize_failure_transient(hass, device_light_1, coordinator):
|
||||
"""Test ZHA failing to initialize the gateway but with a transient error."""
|
||||
zha_gateway = get_zha_gateway(hass)
|
||||
assert zha_gateway is not None
|
||||
|
||||
with patch(
|
||||
"bellows.zigbee.application.ControllerApplication.new",
|
||||
side_effect=[RuntimeError(), zigpy.exceptions.TransientConnectionError()],
|
||||
) as mock_new:
|
||||
with pytest.raises(ConfigEntryNotReady):
|
||||
await zha_gateway.async_initialize()
|
||||
|
||||
# Initialization immediately stops and is retried after TransientConnectionError
|
||||
assert mock_new.call_count == 2
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user