Compare commits

..

74 Commits

Author SHA1 Message Date
Paulus Schoutsen 3bb9be2382 2023.1.4 (#85764) 2023-01-12 15:00:21 -05:00
Paulus Schoutsen 6581bad7ce Bumped version to 2023.1.4 2023-01-12 13:20:23 -05:00
Shay Levy 197634503f Bump aiowebostv to 0.3.0 (#85756) 2023-01-12 13:20:17 -05:00
Franck Nijhof 32fc0e03a5 Use jemalloc in Docker builds (#85738) 2023-01-12 13:20:16 -05:00
Nathan Spencer 2e9ea0c934 Fix Litter-Robot 4 firmware versions reported while updating (#85710) 2023-01-12 13:20:15 -05:00
Nathan Spencer 856f68252b Use built in polling for litterrobot update entity (#84678)
* Use built in polling

* Define scan interval
2023-01-12 13:19:55 -05:00
Nathan Spencer 2789747b0f Bump pylitterbot to 2023.1.0 (#85484) 2023-01-12 13:18:37 -05:00
Daniel Hjelseth Høyer 45d14739c5 Update pyTibber to 0.26.8 (#85702)
* Tibber, update pyTibber

Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>

* Tibber, update pyTibber

Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>

Signed-off-by: Daniel Hjelseth Høyer <github@dahoiv.net>
2023-01-12 13:17:40 -05:00
Ville Skyttä d0f95d84b4 Upgrade huawei-lte-api to 1.6.11 (#85669) 2023-01-12 13:17:38 -05:00
Robert Hillis 1e852e761c Remove oauth2client dependency in Google Sheets (#85637)
Remove oauth2client dependency
2023-01-12 13:17:37 -05:00
Ville Skyttä c3859f9170 Improve Huawei LTE SSDP inclusion (#85572)
* Probe Huawei LTE API for device support on SSDP match

More or less as expected, the loosening of SSDP/UPnP data matches done
in #81643 started to yield false positives, as in #85402.

Coming up with robust matches solely based on the SSDP/UPnP data still
does not seem possible, so keep the matches as loose as they were made,
but additionally invoke a probe request on the API to determine if the
device looks like a supported one.

* Probe only after unique id checks

Prevents throwaway probes for discoveries already in progress.

* Fix SSDP result URL test, add missing assert on it
2023-01-12 13:17:36 -05:00
Milan Meulemans 6df4fc6708 Upgrade aionanoleaf to 0.2.1 (#83669) 2023-01-12 13:17:34 -05:00
Paulus Schoutsen b297b78086 2023.1.3 (#85645) 2023-01-10 15:58:26 -05:00
Paulus Schoutsen 4bdf87d383 Bumped version to 2023.1.3 2023-01-10 14:52:06 -05:00
Paul Bottein e47364f34d Update frontend to 20230110.0 (#85640) 2023-01-10 14:52:01 -05:00
Erik Montnemery fe7d32dc5d Bump hatasmota to 0.6.3 (#85633) 2023-01-10 14:52:00 -05:00
Allen Porter 62a003a053 Bump gcal-sync to 4.1.2 (#85631) 2023-01-10 14:51:59 -05:00
Shay Levy b5d1421dfd Bump aioshelly to 5.2.1 to fix Task exception was never retrieved (#85575)
Bump aioshelly to 5.2.1
2023-01-10 14:51:58 -05:00
Aaron Bach ebab2bd0f9 Remove no-longer-needed invalid API key monitor for OpenUV (#85573)
* Remove no-longer-needed invalid API key monitor for OpenUV

* Handle re-auth cancellation

* Use automatic API status check
2023-01-10 14:51:57 -05:00
J. Nick Koston e7babb4266 Do not check ble scanner state for sleepy shelly devices (#85566)
fixes #85563
2023-01-10 14:51:56 -05:00
Christopher Bailey 1a042c2dad Bump pyunifiprotect to 4.6.1 (#85547) 2023-01-10 14:51:55 -05:00
Keilin Bickar 731ca046f6 Bump sense_energy to 0.11.1 (#85533)
fixes undefined
2023-01-10 14:51:54 -05:00
epenet c844276e95 Remove invalid state class in Subaru sensor (#85520) 2023-01-10 14:51:53 -05:00
starkillerOG 9f9cdb62eb Restore Netgear signal strength icon (#85512) 2023-01-10 14:51:52 -05:00
Franck Nijhof c73830439f Remove invalid Signal Strength device class from NETGEAR (#85510) 2023-01-10 14:51:51 -05:00
Christopher Bailey 940b5d62b4 Bump pyunifiprotect to 4.6.0 (#85483) 2023-01-10 14:51:50 -05:00
Paulus Schoutsen b3454bfd9c 2023.1.2 (#85481) 2023-01-08 21:44:13 -05:00
Paulus Schoutsen 834847988d Bumped version to 2023.1.2 2023-01-08 20:24:25 -05:00
Allen Porter caf15534bb Bump gcal_sync to 4.1.1 (#85453) 2023-01-08 20:24:19 -05:00
Allen Porter 10cb2e31c4 Bump ical to 4.2.9 (#85401) 2023-01-08 20:24:18 -05:00
Lutz Lengemann 85c9f9facf Increase Hydrawise default scan interval (#85398)
Increasing default scan interval

Fixes #83540
2023-01-08 20:24:17 -05:00
J. Nick Koston 5ff7b3bb1a Bump pySwitchbot to 0.36.3 (#85360) 2023-01-08 20:24:16 -05:00
J. Nick Koston e5ba423d6d Add note to SwitchBot locks that usernames are case sensitive (#85359) 2023-01-08 20:24:15 -05:00
puddly b30d4ef7cf Bump ZHA dependencies (#85355)
* Bump ZHA dependencies

* Deprecated `foundation.Command` -> `foundation.GeneralCommand`
2023-01-08 20:24:15 -05:00
Joakim Plate 00e563f1b8 Switch play pause method in philips js (#85343)
fixes undefined
2023-01-08 20:24:14 -05:00
Phil Bruckner cf06f3b81d Bump life360 package to 5.5.0 (#85322)
Improve debug output & redact sensitive info from log.
Fix bug that was masking some HTTP errors.
Retry HTTP errors 502, 503 & 504, which have been observed to happen every once
in a while, resulting in fewer unnecessary unavailable states.
2023-01-08 20:24:13 -05:00
starkillerOG a781fcca86 Bump reolink-aio to 0.1.3 (#85309)
bump reolink-aio to 0.1.3
2023-01-08 20:24:12 -05:00
Tom Puttemans 764550f2e1 Fix dsmr_reader peak hour consumption unit of measurement (#85301) 2023-01-08 20:24:11 -05:00
puddly 7396bcc585 Retry ZHA config entry setup when ENETUNREACH is caught (#84615)
* The config entry is not ready on `ENETUNREACH`

* Use new `TransientConnectionError` from zigpy
2023-01-08 20:20:26 -05:00
epenet 7e6b087773 Allow SensorDeviceClass.POWER_FACTOR unit None (#85287)
Co-authored-by: Erik Montnemery <erik@montnemery.com>
2023-01-06 09:27:02 +01:00
Paulus Schoutsen 71ce7373a3 2023.1.1 (#85277) 2023-01-05 23:18:21 -05:00
Paulus Schoutsen 33bb9c230b Bumped version to 2023.1.1 2023-01-05 22:24:17 -05:00
Charles Garwood f0f2c12d91 Fix Fully Kiosk service call config entry handling (#85275)
* Make sure we're getting the fully_kiosk config entry

* Make sure we're getting the fully_kiosk config entry
2023-01-05 22:23:52 -05:00
J. Nick Koston 2840821594 Reject the WiFI AP when considering to update a shelly config entry from zeroconf (#85265)
Reject the WiFI AP IP when considering to update a shelly config entry from zeroconf

fixes #85180
2023-01-05 22:23:51 -05:00
rikroe edfd83c3a7 Bump bimmer_connected to 0.12.0 (#85255)
* Bump bimmer_connected to 0.12.0

* Fix mypy

* Remove not needed code

Co-authored-by: rikroe <rikroe@users.noreply.github.com>
2023-01-05 22:23:51 -05:00
starkillerOG ee88f34a91 bump reolink-aio to 0.1.2 (#85247) 2023-01-05 22:22:35 -05:00
J. Nick Koston fa4c250001 Improve error reporting when switchbot auth fails (#85244)
* Improve error reporting when switchbot auth fails

related issue #85243

* bump

* coverage
2023-01-05 22:21:55 -05:00
Franck Nijhof 59d6f827c3 Fix device class for DSMR gas sensors providing energy readings (#85202) 2023-01-05 22:18:30 -05:00
epenet 26ea02aa8f Remove invalid device class for RSSI sensors (#85191)
* Remove invalid device class for RRSI sensors

* Restore state class
2023-01-05 22:18:29 -05:00
epenet d73b86132b Adjust valid energy units (#85190) 2023-01-05 22:18:28 -05:00
Franck Nijhof 8034faadca Remove invalid AQI unit from Environment Canada (#85183) 2023-01-05 22:18:27 -05:00
Erik Montnemery 3c2b7c0d69 Bump hatasmota to 0.6.2 (#85182) 2023-01-05 22:18:26 -05:00
Ernst Klamer 563ad02c65 Bump bthome-ble to 2.4.1 (#85153)
fix https://github.com/home-assistant/core/issues/85142
fixes undefined
2023-01-05 22:15:13 -05:00
Michal Čihař fe89b663e7 Fix lacrosse_view fetching of latest data (#85117)
lacrosse_view: fixed fetching of latest data

When using datetime.utcnow(), it only replaces timezone information with
UTC making the actual time offset by the timezone. When you are in UTC-
timezones, it makes no issue as the offset is in the future, but when in
UTC+, the last hour(s) of data are missing.

This commits swtiches to time.time() as UTC timestamp is actually what
the API expects.

It also reduces the window to one hour what noticeably improves the API
performance.
2023-01-05 22:15:12 -05:00
William Scanlon dcd07d3135 Bump pyeconet to 0.1.18 to fix energy usage (#85094) 2023-01-05 22:15:11 -05:00
Robert Svensson 8bf2299407 Only subscribe to relevant IDs for state updates (#85252)
Make sure to only subscribe to the relevant ID
2023-01-05 21:49:22 -05:00
Robert Svensson 9c689d757c Limit calls in UniFi to write state (#85248)
Limit calls to write state to when relevant
2023-01-05 15:38:24 -05:00
Franck Nijhof 4e4fc1767f 2023.1.0 (#85120) 2023-01-04 20:47:22 +01:00
Martin Hjelmare cc3c5772c5 Fix Z-Wave JS sensor units and device classes (#85129)
fixes undefined
2023-01-04 19:47:59 +01:00
J. Nick Koston 6ba6991ecd Bump home-assistant-bluetooth to 1.9.2 (#85123) 2023-01-04 18:36:15 +01:00
Franck Nijhof d52d068469 Merge branch 'master' into rc 2023-01-04 17:09:15 +01:00
Franck Nijhof 09b3611a63 Bumped version to 2023.1.0 2023-01-04 17:05:21 +01:00
Joakim Sørensen ab2f05d3e9 Handle zone exception when setting up Cloudflare (#85110) 2023-01-04 17:04:43 +01:00
Guido Schmitz 90ac0c870f Remove illuminance device class for sensors in devolo Home Control (#85108) 2023-01-04 17:04:40 +01:00
Bram Kragten 0fd113db59 Update frontend to 20230104.0 (#85107) 2023-01-04 17:04:35 +01:00
Jan Bouwhuis 1b43323f5e Allow MQTT device_class or state_class to be set as None (#85106)
* Allow MQTT device_class to be set as `None`

* Add test

* Also allow sensor state_class to be `None`
2023-01-04 17:04:30 +01:00
Jan Bouwhuis 6108e581b1 Do not reset current selection on reconfig or MQTT select (#85099)
* Do not reset current selection on reconfig

* Add a test
2023-01-04 11:54:27 +01:00
Aaron Bach c8c68f05ec Remove workaround for reloading PurpleAir upon device removal (#85086) 2023-01-04 11:53:06 +01:00
Franck Nijhof b80467dc58 Update adguard to 0.6.1 (#85052)
* Update adguard to 0.6.0

* Update adguard to 0.6.1
2023-01-04 11:49:16 +01:00
Andre Lengwenus 6e9f0eca03 Fix integer only LCN variable values (#85035) 2023-01-04 11:49:10 +01:00
Paulus Schoutsen cc6a2f0338 2022.12.9 (#85030) 2023-01-02 22:06:36 -05:00
J. Nick Koston 6ebf2ec9ec Fix failing HomeKit Controller diagnostics tests (#84936) 2023-01-02 22:05:25 -05:00
Paulus Schoutsen 3ba59fbebe Bumped version to 2022.12.9 2023-01-02 20:30:09 -05:00
Martin Hjelmare f3fab5c1f5 Handle not available add-on in hassio add-on manager (#84943)
* Handle not available add-on in hassio add-on manager

* Fix zwave_js tests

* Fix sky connect tests

* Fix matter tests

* Fix yellow tests

* Update hardware tests
2023-01-02 20:30:03 -05:00
101 changed files with 843 additions and 467 deletions
+31 -8
View File
@@ -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,
+16
View File
@@ -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",
+4 -4
View File
@@ -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,
+10 -5
View File
@@ -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
+1 -1
View File
@@ -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"]
+2 -2
View File
@@ -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",
+18 -41
View File
@@ -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,
+2 -2
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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),
+1 -1
View File
@@ -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]
+2 -2
View File
@@ -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)
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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"]
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -213,7 +213,7 @@ class NumberDeviceClass(StrEnum):
POWER_FACTOR = "power_factor"
"""Power factor.
Unit of measurement: `%`
Unit of measurement: `%`, `None`
"""
POWER = "power"
+3 -9
View File
@@ -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()
+15 -66
View File
@@ -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."""
+2 -23
View File
@@ -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"]
+1 -1
View File
@@ -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": [
+2 -2
View File
@@ -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),
+13 -1
View File
@@ -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,
+8 -2
View File
@@ -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:
+11 -2
View File
@@ -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:
+13 -3
View File
@@ -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:
+7 -1
View File
@@ -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)",
+2 -2
View File
@@ -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"
-1
View File
@@ -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",
+149 -29
View File
@@ -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:
+1 -1
View File
@@ -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)
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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."""
+10 -2
View File
@@ -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
+55
View File
@@ -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(
+32 -1
View File
@@ -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()
+13
View File
@@ -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
+26 -1
View File
@@ -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"]
+14
View File
@@ -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):
+15 -1
View File
@@ -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
+1 -1
View File
@@ -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
+19
View File
@@ -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